authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-05-08 11:52:19-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-05-08 11:52:19-07:00
log825263b6f979e24bc7cd48a8ea0bdf18c27b58fa
tree8124baa8def24f108cb59c53003169b226b434e9
parent25b02f4ba28d9c6a1adff3475625e5b3fdacce4b
signature Signed by PGP key B5690EEEBB952194

Scaffold oxlint plugin with initial rule (#2603)

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

86 files changed, 1137 insertions(+), 455 deletions(-)

.oxlintrc.json+4-1
......@@ -9,6 +9,7 @@
99 "**/routeTree.gen.ts"
1010 ],
1111 "plugins": ["eslint", "oxc", "react", "unicorn", "typescript"],
12 "jsPlugins": ["./oxlint-plugin-t3code/index.ts"],
1213 "categories": {
1314 "correctness": "warn",
1415 "suspicious": "warn",
......@@ -17,6 +18,8 @@
1718 "rules": {
1819 "react-in-jsx-scope": "off",
1920 "eslint/no-shadow": "off",
20 "eslint/no-await-in-loop": "off"
21 "eslint/no-await-in-loop": "off",
22 "eslint/no-underscore-dangle": "off",
23 "t3code/no-inline-schema-compile": "warn"
2124 }
2225}
apps/desktop/src/app/DesktopAppIdentity.ts+2-1
......@@ -16,6 +16,7 @@ const COMMIT_HASH_DISPLAY_LENGTH = 12;
1616const AppPackageMetadata = Schema.Struct({
1717 t3codeCommitHash: Schema.optional(Schema.String),
1818});
19const decodeAppPackageMetadata = Schema.decodeEffect(Schema.fromJsonString(AppPackageMetadata));
1920
2021export interface DesktopAppIdentityShape {
2122 readonly resolveUserDataPath: Effect.Effect<string>;
......@@ -47,7 +48,7 @@ const make = Effect.gen(function* () {
4748 return yield* Option.match(raw, {
4849 onNone: () => Effect.succeed(Option.none<string>()),
4950 onSome: (value) =>
50 Schema.decodeEffect(Schema.fromJsonString(AppPackageMetadata))(value).pipe(
51 decodeAppPackageMetadata(value).pipe(
5152 Effect.map((parsed) =>
5253 Option.fromNullishOr(parsed.t3codeCommitHash).pipe(Option.flatMap(normalizeCommitHash)),
5354 ),
apps/desktop/src/backend/DesktopBackendManager.test.ts+5-1
......@@ -25,6 +25,10 @@ import * as DesktopObservability from "../app/DesktopObservability.ts";
2525import * as DesktopState from "../app/DesktopState.ts";
2626import * as DesktopWindow from "../window/DesktopWindow.ts";
2727
28const decodeDesktopBackendBootstrap = Schema.decodeEffect(
29 Schema.fromJsonString(DesktopBackendBootstrap),
30);
31
2832const baseConfig: DesktopBackendManager.DesktopBackendStartConfig = {
2933 executablePath: "/electron",
3034 entryPath: "/server/bin.mjs",
......@@ -94,7 +98,7 @@ const healthyHttpClientLayer = httpClientLayer((request) =>
9498);
9599
96100function decodeBootstrap(raw: string) {
97 return Schema.decodeEffect(Schema.fromJsonString(DesktopBackendBootstrap))(raw);
101 return decodeDesktopBackendBootstrap(raw);
98102}
99103
100104function makeManagerLayer(input: {
apps/desktop/src/settings/DesktopClientSettings.ts+6-2
......@@ -20,10 +20,14 @@ const ClientSettingsDocumentSchema = Schema.Struct({
2020
2121const ClientSettingsJson = fromLenientJson(ClientSettingsSchema);
2222const LegacyClientSettingsDocumentJson = fromLenientJson(ClientSettingsDocumentSchema);
23const decodeLegacyClientSettingsDocumentJson = Schema.decodeEffect(
24 LegacyClientSettingsDocumentJson,
25);
26const decodeClientSettingsJsonValue = Schema.decodeEffect(ClientSettingsJson);
2327const decodeClientSettingsJson = (raw: string): Effect.Effect<ClientSettings, Schema.SchemaError> =>
24 Schema.decodeEffect(LegacyClientSettingsDocumentJson)(raw).pipe(
28 decodeLegacyClientSettingsDocumentJson(raw).pipe(
2529 Effect.map((document) => document.settings),
26 Effect.catch(() => Schema.decodeEffect(ClientSettingsJson)(raw)),
30 Effect.catch(() => decodeClientSettingsJsonValue(raw)),
2731 );
2832const encodeClientSettingsJson = Schema.encodeEffect(ClientSettingsJson);
2933
apps/desktop/src/updates/DesktopUpdates.ts+7-4
......@@ -54,6 +54,9 @@ const UpdateInfo = Schema.Struct({
5454const DownloadProgressInfo = Schema.Struct({
5555 percent: Schema.Number,
5656});
57const decodeAppUpdateYmlConfig = Schema.decodeUnknownEffect(AppUpdateYmlConfig);
58const decodeUpdateInfo = Schema.decodeUnknownEffect(UpdateInfo);
59const decodeDownloadProgressInfo = Schema.decodeUnknownEffect(DownloadProgressInfo);
5760
5861const currentIsoTimestamp = DateTime.now.pipe(Effect.map(DateTime.formatIso));
5962
......@@ -115,7 +118,7 @@ function parseAppUpdateYml(raw: string): Effect.Effect<Option.Option<AppUpdateYm
115118 }
116119 }
117120
118 return Schema.decodeUnknownEffect(AppUpdateYmlConfig)(entries).pipe(
121 return decodeAppUpdateYmlConfig(entries).pipe(
119122 Effect.map((config) => (config.provider ? Option.some(config) : Option.none())),
120123 Effect.catch(() => Effect.succeed(Option.none<AppUpdateYmlConfig>())),
121124 );
......@@ -408,7 +411,7 @@ const make = Effect.gen(function* () {
408411 const handleUpdateAvailable = Effect.fn("desktop.updates.handleUpdateAvailable")(function* (
409412 raw: unknown,
410413 ) {
411 yield* Schema.decodeUnknownEffect(UpdateInfo)(raw).pipe(
414 yield* decodeUpdateInfo(raw).pipe(
412415 Effect.flatMap(
413416 Effect.fn("desktop.updates.applyUpdateAvailable")(function* (info) {
414417 const state = yield* Ref.get(updateStateRef);
......@@ -479,7 +482,7 @@ const make = Effect.gen(function* () {
479482 const handleDownloadProgress = Effect.fn("desktop.updates.handleDownloadProgress")(function* (
480483 raw: unknown,
481484 ) {
482 yield* Schema.decodeUnknownEffect(DownloadProgressInfo)(raw).pipe(
485 yield* decodeDownloadProgressInfo(raw).pipe(
483486 Effect.flatMap(
484487 Effect.fn("desktop.updates.applyDownloadProgress")(function* (progress) {
485488 const state = yield* Ref.get(updateStateRef);
......@@ -506,7 +509,7 @@ const make = Effect.gen(function* () {
506509 const handleUpdateDownloaded = Effect.fn("desktop.updates.handleUpdateDownloaded")(function* (
507510 raw: unknown,
508511 ) {
509 yield* Schema.decodeUnknownEffect(UpdateInfo)(raw).pipe(
512 yield* decodeUpdateInfo(raw).pipe(
510513 Effect.flatMap(
511514 Effect.fn("desktop.updates.applyUpdateDownloaded")(function* (info) {
512515 const state = yield* Ref.get(updateStateRef);
apps/server/integration/OrchestrationEngineHarness.integration.ts+3-1
......@@ -79,6 +79,8 @@ import { VcsStatusBroadcaster } from "../src/vcs/VcsStatusBroadcaster.ts";
7979import { GitWorkflowService } from "../src/git/GitWorkflowService.ts";
8080import * as VcsProcess from "../src/vcs/VcsProcess.ts";
8181
82const decodeCodexSettings = Schema.decodeEffect(CodexSettings);
83
8284function runGit(cwd: string, args: ReadonlyArray<string>) {
8385 return execFileSync("git", args, {
8486 cwd,
......@@ -266,7 +268,7 @@ export const makeOrchestrationIntegrationHarness = (
266268 const realCodexRegistry = Layer.effect(
267269 ProviderAdapterRegistry,
268270 Effect.gen(function* () {
269 const codexSettings = Schema.decodeSync(CodexSettings)({});
271 const codexSettings = yield* decodeCodexSettings({});
270272 const codexAdapter = yield* makeCodexAdapter(codexSettings);
271273 return makeAdapterRegistryMock({
272274 [ProviderDriverKind.make("codex")]: codexAdapter,
apps/server/src/bootstrap.test.ts+3-6
......@@ -38,6 +38,7 @@ vi.mock("node:fs", async (importOriginal) => {
3838});
3939
4040const TestEnvelopeSchema = Schema.Struct({ mode: Schema.String });
41const encodeTestEnvelopeSchema = Schema.encodeEffect(Schema.fromJsonString(TestEnvelopeSchema));
4142
4243it.layer(NodeServices.layer)("readBootstrapEnvelope", (it) => {
4344 it.effect("uses platform-specific fd paths", () =>
......@@ -55,9 +56,7 @@ it.layer(NodeServices.layer)("readBootstrapEnvelope", (it) => {
5556
5657 yield* fs.writeFileString(
5758 filePath,
58 `${yield* Schema.encodeEffect(Schema.fromJsonString(TestEnvelopeSchema))({
59 mode: "desktop",
60 })}\n`,
59 `${yield* encodeTestEnvelopeSchema({ mode: "desktop" })}\n`,
6160 );
6261
6362 const fd = yield* Effect.acquireRelease(
......@@ -79,9 +78,7 @@ it.layer(NodeServices.layer)("readBootstrapEnvelope", (it) => {
7978
8079 yield* fs.writeFileString(
8180 filePath,
82 `${yield* Schema.encodeEffect(Schema.fromJsonString(TestEnvelopeSchema))({
83 mode: "desktop",
84 })}\n`,
81 `${yield* encodeTestEnvelopeSchema({ mode: "desktop" })}\n`,
8582 );
8683
8784 // Open without acquireRelease: the direct-stream fallback uses autoClose: true,
apps/server/src/diagnostics/ProcessDiagnostics.ts+2-1
......@@ -50,6 +50,7 @@ class ProcessDiagnosticsError extends Schema.TaggedErrorClass<ProcessDiagnostics
5050 cause: Schema.optional(Schema.Defect),
5151 },
5252) {}
53const isProcessDiagnosticsError = Schema.is(ProcessDiagnosticsError);
5354
5455function toProcessDiagnosticsError(message: string, cause?: unknown): ProcessDiagnosticsError {
5556 return new ProcessDiagnosticsError({
......@@ -316,7 +317,7 @@ const runProcess = Effect.fn("runProcess")(
316317 }),
317318 ),
318319 Effect.mapError((cause) =>
319 Schema.is(ProcessDiagnosticsError)(cause)
320 isProcessDiagnosticsError(cause)
320321 ? cause
321322 : toProcessDiagnosticsError(input.errorMessage, cause),
322323 ),
apps/server/src/keybindings.test.ts+10-4
......@@ -23,6 +23,12 @@ import {
2323import { KeybindingsConfigError } from "@t3tools/contracts";
2424
2525const KeybindingsConfigJson = Schema.fromJsonString(KeybindingsConfig);
26const encodeKeybindingsConfigJson = Schema.encodeEffect(KeybindingsConfigJson);
27const decodeKeybindingsConfigJson = Schema.decodeUnknownEffect(KeybindingsConfigJson);
28const encodeResolvedKeybindingFromConfig = Schema.encodeEffect(ResolvedKeybindingFromConfig);
29const decodeResolvedKeybindingFromConfigExit = Schema.decodeUnknownExit(
30 ResolvedKeybindingFromConfig,
31);
2632const makeKeybindingsLayer = () => {
2733 return KeybindingsLive.pipe(
2834 Layer.provideMerge(
......@@ -45,7 +51,7 @@ const writeKeybindingsConfig = (configPath: string, rules: readonly KeybindingRu
4551 Effect.gen(function* () {
4652 const fileSystem = yield* FileSystem.FileSystem;
4753 const path = yield* Path.Path;
48 const encoded = yield* Schema.encodeEffect(KeybindingsConfigJson)(rules);
54 const encoded = yield* encodeKeybindingsConfigJson(rules);
4955 yield* fileSystem.makeDirectory(path.dirname(configPath), { recursive: true });
5056 yield* fileSystem.writeFileString(configPath, encoded);
5157 });
......@@ -54,7 +60,7 @@ const readKeybindingsConfig = (configPath: string) =>
5460 Effect.gen(function* () {
5561 const fileSystem = yield* FileSystem.FileSystem;
5662 const rawConfig = yield* fileSystem.readFileString(configPath);
57 return yield* Schema.decodeUnknownEffect(KeybindingsConfigJson)(rawConfig);
63 return yield* decodeKeybindingsConfigJson(rawConfig);
5864 });
5965
6066it.layer(NodeServices.layer)("keybindings", (it) => {
......@@ -111,7 +117,7 @@ it.layer(NodeServices.layer)("keybindings", (it) => {
111117
112118 it.effect("encodes resolved plus-key shortcuts", () =>
113119 Effect.gen(function* () {
114 const encoded = yield* Schema.encodeEffect(ResolvedKeybindingFromConfig)({
120 const encoded = yield* encodeResolvedKeybindingFromConfig({
115121 command: "terminal.toggle",
116122 shortcut: {
117123 key: "+",
......@@ -157,7 +163,7 @@ it.layer(NodeServices.layer)("keybindings", (it) => {
157163
158164 it.effect("formats invalid resolved keybinding rules with the custom message", () =>
159165 Effect.sync(() => {
160 const result = Schema.decodeUnknownExit(ResolvedKeybindingFromConfig)({
166 const result = decodeResolvedKeybindingFromConfigExit({
161167 key: "mod+shift+d+o",
162168 command: "terminal.new",
163169 });
apps/server/src/keybindings.ts+10-6
......@@ -170,6 +170,10 @@ function encodeWhenAst(node: KeybindingWhenNode): string {
170170
171171const RawKeybindingsEntries = fromLenientJson(Schema.Array(Schema.Unknown));
172172const KeybindingsConfigPrettyJson = fromJsonStringPretty(KeybindingsConfig);
173const decodeKeybindingRuleExit = Schema.decodeUnknownExit(KeybindingRule);
174const decodeResolvedKeybindingFromConfigExit = Schema.decodeExit(ResolvedKeybindingFromConfig);
175const decodeRawKeybindingsEntriesExit = Schema.decodeUnknownExit(RawKeybindingsEntries);
176const encodeKeybindingsConfigPrettyJson = Schema.encodeEffect(KeybindingsConfigPrettyJson);
173177
174178export interface KeybindingsConfigState {
175179 readonly keybindings: ResolvedKeybindingsConfig;
......@@ -344,7 +348,7 @@ const makeKeybindings = Effect.gen(function* () {
344348
345349 return yield* Effect.forEach(rawConfig, (entry) =>
346350 Effect.gen(function* () {
347 const decodedRule = Schema.decodeUnknownExit(KeybindingRule)(entry);
351 const decodedRule = decodeKeybindingRuleExit(entry);
348352 if (decodedRule._tag === "Failure") {
349353 yield* Effect.logWarning("ignoring invalid keybinding entry", {
350354 path: keybindingsConfigPath,
......@@ -353,7 +357,7 @@ const makeKeybindings = Effect.gen(function* () {
353357 });
354358 return null;
355359 }
356 const resolved = Schema.decodeExit(ResolvedKeybindingFromConfig)(decodedRule.value);
360 const resolved = decodeResolvedKeybindingFromConfigExit(decodedRule.value);
357361 if (resolved._tag === "Failure") {
358362 yield* Effect.logWarning("ignoring invalid keybinding entry", {
359363 path: keybindingsConfigPath,
......@@ -379,7 +383,7 @@ const makeKeybindings = Effect.gen(function* () {
379383 }
380384
381385 const rawConfig = yield* readRawConfig;
382 const decodedEntries = Schema.decodeUnknownExit(RawKeybindingsEntries)(rawConfig);
386 const decodedEntries = decodeRawKeybindingsEntriesExit(rawConfig);
383387 if (decodedEntries._tag === "Failure") {
384388 const detail = `expected JSON array (${Cause.pretty(decodedEntries.cause)})`;
385389 return {
......@@ -391,7 +395,7 @@ const makeKeybindings = Effect.gen(function* () {
391395 const keybindings: KeybindingRule[] = [];
392396 const issues: ServerConfigIssue[] = [];
393397 for (const [index, entry] of decodedEntries.value.entries()) {
394 const decodedRule = Schema.decodeUnknownExit(KeybindingRule)(entry);
398 const decodedRule = decodeKeybindingRuleExit(entry);
395399 if (decodedRule._tag === "Failure") {
396400 const detail = Cause.pretty(decodedRule.cause);
397401 issues.push(invalidEntryIssue(index, detail));
......@@ -404,7 +408,7 @@ const makeKeybindings = Effect.gen(function* () {
404408 continue;
405409 }
406410
407 const resolvedRule = Schema.decodeExit(ResolvedKeybindingFromConfig)(decodedRule.value);
411 const resolvedRule = decodeResolvedKeybindingFromConfigExit(decodedRule.value);
408412 if (resolvedRule._tag === "Failure") {
409413 const detail = Cause.pretty(resolvedRule.cause);
410414 issues.push(invalidEntryIssue(index, detail));
......@@ -423,7 +427,7 @@ const makeKeybindings = Effect.gen(function* () {
423427 });
424428
425429 const writeConfigAtomically = (rules: readonly KeybindingRule[]) => {
426 return Schema.encodeEffect(KeybindingsConfigPrettyJson)(rules).pipe(
430 return encodeKeybindingsConfigPrettyJson(rules).pipe(
427431 Effect.map((encoded) => `${encoded}\n`),
428432 Effect.flatMap((encoded) =>
429433 writeFileStringAtomically({
apps/server/src/orchestration/Layers/OrchestrationEngine.ts+6-2
......@@ -44,6 +44,10 @@ import {
4444 OrchestrationEngineService,
4545 type OrchestrationEngineShape,
4646} from "../Services/OrchestrationEngine.ts";
47const isOrchestrationCommandPreviouslyRejectedError = Schema.is(
48 OrchestrationCommandPreviouslyRejectedError,
49);
50const isOrchestrationCommandInvariantError = Schema.is(OrchestrationCommandInvariantError);
4751
4852interface CommandEnvelope {
4953 command: OrchestrationCommand;
......@@ -245,7 +249,7 @@ const makeOrchestrationEngine = Effect.gen(function* () {
245249 }
246250
247251 const error = Cause.squash(exit.cause) as OrchestrationDispatchError;
248 if (!Schema.is(OrchestrationCommandPreviouslyRejectedError)(error)) {
252 if (!isOrchestrationCommandPreviouslyRejectedError(error)) {
249253 yield* reconcileReadModelAfterDispatchFailure.pipe(
250254 Effect.catch(() =>
251255 Effect.logWarning(
......@@ -259,7 +263,7 @@ const makeOrchestrationEngine = Effect.gen(function* () {
259263 ),
260264 );
261265
262 if (Schema.is(OrchestrationCommandInvariantError)(error)) {
266 if (isOrchestrationCommandInvariantError(error)) {
263267 yield* commandReceiptRepository
264268 .upsert({
265269 commandId: envelope.command.commandId,
apps/server/src/orchestration/Layers/ProviderCommandReactor.ts+5-3
......@@ -39,6 +39,8 @@ import {
3939import { ServerSettingsService } from "../../serverSettings.ts";
4040import { VcsStatusBroadcaster } from "../../vcs/VcsStatusBroadcaster.ts";
4141import { GitWorkflowService } from "../../git/GitWorkflowService.ts";
42const isProviderAdapterRequestError = Schema.is(ProviderAdapterRequestError);
43const isProviderDriverKind = Schema.is(ProviderDriverKind);
4244
4345type ProviderIntentEvent = Extract<
4446 OrchestrationEvent,
......@@ -118,7 +120,7 @@ function findProviderAdapterRequestError(
118120 cause: Cause.Cause<ProviderServiceError>,
119121): ProviderAdapterRequestError | undefined {
120122 const failReason = cause.reasons.find(Cause.isFailReason);
121 return Schema.is(ProviderAdapterRequestError)(failReason?.error) ? failReason.error : undefined;
123 return isProviderAdapterRequestError(failReason?.error) ? failReason.error : undefined;
122124}
123125
124126function isUnknownPendingApprovalRequestError(cause: Cause.Cause<ProviderServiceError>): boolean {
......@@ -233,7 +235,7 @@ const make = Effect.gen(function* () {
233235
234236 const formatFailureDetail = (cause: Cause.Cause<unknown>): string => {
235237 const failReason = cause.reasons.find(Cause.isFailReason);
236 const providerError = Schema.is(ProviderAdapterRequestError)(failReason?.error)
238 const providerError = isProviderAdapterRequestError(failReason?.error)
237239 ? failReason.error
238240 : undefined;
239241 if (providerError) {
......@@ -361,7 +363,7 @@ const make = Effect.gen(function* () {
361363 ),
362364 );
363365 const desiredDriverKind = desiredInfo.driverKind;
364 if (!Schema.is(ProviderDriverKind)(desiredDriverKind)) {
366 if (!isProviderDriverKind(desiredDriverKind)) {
365367 return yield* new ProviderAdapterRequestError({
366368 provider: providerErrorLabel(String(desiredDriverKind)),
367369 method: "thread.turn.start",
apps/server/src/orchestration/projector.ts+4-5
......@@ -47,15 +47,14 @@ function updateThread(
4747}
4848
4949function decodeForEvent<A>(
50 schema: Schema.Schema<A>,
50 schema: Schema.Decoder<A, never>,
5151 value: unknown,
5252 eventType: OrchestrationEvent["type"],
5353 field: string,
5454): Effect.Effect<A, OrchestrationProjectorDecodeError> {
55 return Effect.try({
56 try: () => Schema.decodeUnknownSync(schema as any)(value),
57 catch: (error) => toProjectorDecodeError(`${eventType}:${field}`)(error as Schema.SchemaError),
58 });
55 return Schema.decodeUnknownEffect(schema)(value).pipe(
56 Effect.mapError(toProjectorDecodeError(`${eventType}:${field}`)),
57 );
5958}
6059
6160function retainThreadMessagesAfterRevert(
apps/server/src/persistence/Errors.ts+3-1
......@@ -30,6 +30,8 @@ export class PersistenceDecodeError extends Schema.TaggedErrorClass<PersistenceD
3030 return `Decode error in ${this.operation}: ${this.issue}`;
3131 }
3232}
33const isPersistenceSqlError = Schema.is(PersistenceSqlError);
34const isPersistenceDecodeError = Schema.is(PersistenceDecodeError);
3335
3436export function toPersistenceSqlError(operation: string) {
3537 return (cause: unknown): PersistenceSqlError =>
......@@ -59,7 +61,7 @@ export function toPersistenceDecodeCauseError(operation: string) {
5961}
6062
6163export const isPersistenceError = (u: unknown) =>
62 Schema.is(PersistenceSqlError)(u) || Schema.is(PersistenceDecodeError)(u);
64 isPersistenceSqlError(u) || isPersistenceDecodeError(u);
6365
6466// ===============================
6567// Provider Session Repository Errors
apps/server/src/persistence/Layers/OrchestrationEventStore.test.ts+2-1
......@@ -10,6 +10,7 @@ import { PersistenceDecodeError } from "../Errors.ts";
1010import { OrchestrationEventStore } from "../Services/OrchestrationEventStore.ts";
1111import { OrchestrationEventStoreLive } from "./OrchestrationEventStore.ts";
1212import { SqlitePersistenceMemory } from "./Sqlite.ts";
13const isPersistenceDecodeError = Schema.is(PersistenceDecodeError);
1314
1415const layer = it.layer(
1516 OrchestrationEventStoreLive.pipe(Layer.provideMerge(SqlitePersistenceMemory)),
......@@ -110,7 +111,7 @@ layer("OrchestrationEventStore", (it) => {
110111 );
111112 assert.equal(replayResult._tag, "Failure");
112113 if (replayResult._tag === "Failure") {
113 assert.ok(Schema.is(PersistenceDecodeError)(replayResult.failure));
114 assert.ok(isPersistenceDecodeError(replayResult.failure));
114115 assert.ok(
115116 replayResult.failure.operation.includes(
116117 "OrchestrationEventStore.readFromSequence:decodeRows",
apps/server/src/provider/Drivers/ClaudeDriver.ts+2-1
......@@ -48,6 +48,7 @@ import {
4848 resolveProviderMaintenanceCapabilitiesEffect,
4949} from "../providerMaintenance.ts";
5050import { makeClaudeCapabilitiesCacheKey, makeClaudeContinuationGroupKey } from "./ClaudeHome.ts";
51const decodeClaudeSettings = Schema.decodeSync(ClaudeSettings);
5152
5253const DRIVER_KIND = ProviderDriverKind.make("claudeAgent");
5354const SNAPSHOT_REFRESH_INTERVAL = Duration.minutes(5);
......@@ -105,7 +106,7 @@ export const ClaudeDriver: ProviderDriver<ClaudeSettings, ClaudeDriverEnv> = {
105106 supportsMultipleInstances: true,
106107 },
107108 configSchema: ClaudeSettings,
108 defaultConfig: (): ClaudeSettings => Schema.decodeSync(ClaudeSettings)({}),
109 defaultConfig: (): ClaudeSettings => decodeClaudeSettings({}),
109110 create: ({ instanceId, displayName, accentColor, environment, enabled, config }) =>
110111 Effect.gen(function* () {
111112 const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
apps/server/src/provider/Drivers/CodexDriver.ts+2-1
......@@ -51,6 +51,7 @@ import {
5151 materializeCodexShadowHome,
5252 resolveCodexHomeLayout,
5353} from "./CodexHomeLayout.ts";
54const decodeCodexSettings = Schema.decodeSync(CodexSettings);
5455
5556const DRIVER_KIND = ProviderDriverKind.make("codex");
5657const SNAPSHOT_REFRESH_INTERVAL = Duration.minutes(5);
......@@ -103,7 +104,7 @@ export const CodexDriver: ProviderDriver<CodexSettings, CodexDriverEnv> = {
103104 supportsMultipleInstances: true,
104105 },
105106 configSchema: CodexSettings,
106 defaultConfig: (): CodexSettings => Schema.decodeSync(CodexSettings)({}),
107 defaultConfig: (): CodexSettings => decodeCodexSettings({}),
107108 create: ({ instanceId, displayName, accentColor, environment, enabled, config }) =>
108109 Effect.gen(function* () {
109110 const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
apps/server/src/provider/Drivers/CodexHomeLayout.test.ts+2-1
......@@ -11,6 +11,7 @@ import {
1111 materializeCodexShadowHome,
1212 resolveCodexHomeLayout,
1313} from "./CodexHomeLayout.ts";
14const decodeCodexSettingsValue = Schema.decodeSync(CodexSettings);
1415
1516const decodeCodexSettings = (input: {
1617 readonly enabled?: boolean;
......@@ -18,7 +19,7 @@ const decodeCodexSettings = (input: {
1819 readonly shadowHomePath?: string;
1920 readonly customModels?: readonly string[];
2021 readonly binaryPath?: string;
21}): CodexSettings => Schema.decodeSync(CodexSettings)(input);
22}): CodexSettings => decodeCodexSettingsValue(input);
2223
2324const makeTempDir = Effect.fn("CodexHomeLayout.test.makeTempDir")(function* (prefix: string) {
2425 const fileSystem = yield* FileSystem.FileSystem;
apps/server/src/provider/Drivers/CodexHomeLayout.ts+2-1
......@@ -74,6 +74,7 @@ export class CodexShadowHomeError extends Schema.TaggedErrorClass<CodexShadowHom
7474 return this.detail;
7575 }
7676}
77const isCodexShadowHomeError = Schema.is(CodexShadowHomeError);
7778
7879type LinkState =
7980 | {
......@@ -88,7 +89,7 @@ type LinkState =
8889 };
8990
9091function toShadowHomeError(cause: unknown): CodexShadowHomeError {
91 return Schema.is(CodexShadowHomeError)(cause)
92 return isCodexShadowHomeError(cause)
9293 ? cause
9394 : new CodexShadowHomeError({
9495 detail: "Failed to materialize Codex shadow home.",
apps/server/src/provider/Drivers/CursorDriver.ts+2-1
......@@ -45,6 +45,7 @@ import {
4545 makeStaticProviderMaintenanceResolver,
4646 resolveProviderMaintenanceCapabilitiesEffect,
4747} from "../providerMaintenance.ts";
48const decodeCursorSettings = Schema.decodeSync(CursorSettings);
4849
4950const DRIVER_KIND = ProviderDriverKind.make("cursor");
5051const SNAPSHOT_REFRESH_INTERVAL = Duration.minutes(5);
......@@ -89,7 +90,7 @@ export const CursorDriver: ProviderDriver<CursorSettings, CursorDriverEnv> = {
8990 supportsMultipleInstances: true,
9091 },
9192 configSchema: CursorSettings,
92 defaultConfig: (): CursorSettings => Schema.decodeSync(CursorSettings)({}),
93 defaultConfig: (): CursorSettings => decodeCursorSettings({}),
9394 create: ({ instanceId, displayName, accentColor, environment, enabled, config }) =>
9495 Effect.gen(function* () {
9596 const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
apps/server/src/provider/Drivers/OpenCodeDriver.ts+2-1
......@@ -46,6 +46,7 @@ import {
4646 normalizeCommandPath,
4747 resolveProviderMaintenanceCapabilitiesEffect,
4848} from "../providerMaintenance.ts";
49const decodeOpenCodeSettings = Schema.decodeSync(OpenCodeSettings);
4950
5051const DRIVER_KIND = ProviderDriverKind.make("opencode");
5152const SNAPSHOT_REFRESH_INTERVAL = Duration.minutes(5);
......@@ -102,7 +103,7 @@ export const OpenCodeDriver: ProviderDriver<OpenCodeSettings, OpenCodeDriverEnv>
102103 supportsMultipleInstances: true,
103104 },
104105 configSchema: OpenCodeSettings,
105 defaultConfig: (): OpenCodeSettings => Schema.decodeSync(OpenCodeSettings)({}),
106 defaultConfig: (): OpenCodeSettings => decodeOpenCodeSettings({}),
106107 create: ({ instanceId, displayName, accentColor, environment, enabled, config }) =>
107108 Effect.gen(function* () {
108109 const openCodeRuntime = yield* OpenCodeRuntime;
apps/server/src/provider/Layers/ClaudeAdapter.test.ts+4-3
......@@ -38,6 +38,7 @@ import { ServerSettingsService } from "../../serverSettings.ts";
3838import { ProviderAdapterValidationError } from "../Errors.ts";
3939import type { ClaudeAdapterShape } from "../Services/ClaudeAdapter.ts";
4040import { makeClaudeAdapter, type ClaudeAdapterLiveOptions } from "./ClaudeAdapter.ts";
41const decodeClaudeSettings = Schema.decodeSync(ClaudeSettings);
4142
4243// Test-local service tag so the rest of the file can keep using `yield* ClaudeAdapter`.
4344class ClaudeAdapter extends Context.Service<ClaudeAdapter, ClaudeAdapterShape>()(
......@@ -186,7 +187,7 @@ function makeHarness(config?: {
186187 layer: Layer.effect(
187188 ClaudeAdapter,
188189 Effect.gen(function* () {
189 const claudeConfig = Schema.decodeSync(ClaudeSettings)(config?.claudeConfig ?? {});
190 const claudeConfig = decodeClaudeSettings(config?.claudeConfig ?? {});
190191 return yield* makeClaudeAdapter(claudeConfig, adapterOptions);
191192 }),
192193 ).pipe(
......@@ -1344,7 +1345,7 @@ describe("ClaudeAdapterLive", () => {
13441345 const layer = Layer.effect(
13451346 ClaudeAdapter,
13461347 Effect.gen(function* () {
1347 const claudeConfig = Schema.decodeSync(ClaudeSettings)({});
1348 const claudeConfig = decodeClaudeSettings({});
13481349 return yield* makeClaudeAdapter(claudeConfig, {
13491350 createQuery: () => {
13501351 const query = new FakeClaudeQuery();
......@@ -1427,7 +1428,7 @@ describe("ClaudeAdapterLive", () => {
14271428 const layer = Layer.effect(
14281429 ClaudeAdapter,
14291430 Effect.gen(function* () {
1430 const claudeConfig = Schema.decodeSync(ClaudeSettings)({});
1431 const claudeConfig = decodeClaudeSettings({});
14311432 return yield* makeClaudeAdapter(claudeConfig, {
14321433 createQuery: (input) => {
14331434 // Simulate the SDK consuming the prompt iterable
apps/server/src/provider/Layers/ClaudeAdapter.ts+17-18
......@@ -84,6 +84,8 @@ import {
8484} from "../Errors.ts";
8585import { type ClaudeAdapterShape } from "../Services/ClaudeAdapter.ts";
8686import { type EventNdjsonLogger, makeEventNdjsonLogger } from "./EventNdjsonLogger.ts";
87const encodeUnknownJsonStringExit = Schema.encodeUnknownExit(Schema.UnknownFromJsonString);
88const decodeUnknownJsonStringExit = Schema.decodeUnknownExit(Schema.UnknownFromJsonString);
8789
8890const PROVIDER = ProviderDriverKind.make("claudeAgent");
8991type ClaudeTextStreamKind = Extract<RuntimeContentStreamKind, "assistant_text" | "reasoning_text">;
......@@ -93,6 +95,11 @@ type ClaudeToolResultStreamKind = Extract<
9395>;
9496type ClaudeSdkEffort = NonNullable<ClaudeQueryOptions["effort"]>;
9597
98function encodeJsonStringForDiagnostics(input: unknown): string | undefined {
99 const result = encodeUnknownJsonStringExit(input);
100 return Exit.isSuccess(result) ? result.value : undefined;
101}
102
96103type PromptQueueItem =
97104 | {
98105 readonly type: "message";
......@@ -545,7 +552,7 @@ function summarizeToolRequest(toolName: string, input: Record<string, unknown>):
545552 }
546553 }
547554
548 const serialized = Schema.encodeUnknownSync(Schema.UnknownFromJsonString)(input);
555 const serialized = encodeJsonStringForDiagnostics(input) ?? "[unserializable input]";
549556 if (serialized.length <= 400) {
550557 return `${toolName}: ${serialized}`;
551558 }
......@@ -810,22 +817,18 @@ function exitPlanCaptureKey(input: {
810817}
811818
812819function tryParseJsonRecord(value: string): Record<string, unknown> | undefined {
813 try {
814 const parsed = Schema.decodeUnknownSync(Schema.UnknownFromJsonString)(value);
815 return parsed && typeof parsed === "object" && !Array.isArray(parsed)
816 ? (parsed as Record<string, unknown>)
817 : undefined;
818 } catch {
820 const result = decodeUnknownJsonStringExit(value);
821 if (!Exit.isSuccess(result)) {
819822 return undefined;
820823 }
824 const parsed = result.value;
825 return parsed && typeof parsed === "object" && !Array.isArray(parsed)
826 ? (parsed as Record<string, unknown>)
827 : undefined;
821828}
822829
823830function toolInputFingerprint(input: Record<string, unknown>): string | undefined {
824 try {
825 return Schema.encodeUnknownSync(Schema.UnknownFromJsonString)(input);
826 } catch {
827 return undefined;
828 }
831 return encodeJsonStringForDiagnostics(input);
829832}
830833
831834function toolResultStreamKind(itemType: CanonicalItemType): ClaudeToolResultStreamKind | undefined {
......@@ -2941,12 +2944,8 @@ export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* (
29412944 "claude.query.include_partial_messages": true,
29422945 "claude.query.additional_directories": input.cwd ? [input.cwd] : [],
29432946 "claude.query.setting_sources": [...CLAUDE_SETTING_SOURCES],
2944 "claude.query.settings_json": Schema.encodeUnknownSync(Schema.UnknownFromJsonString)(
2945 settings,
2946 ),
2947 "claude.query.extra_args_json": Schema.encodeUnknownSync(Schema.UnknownFromJsonString)(
2948 extraArgs,
2949 ),
2947 "claude.query.settings_json": encodeJsonStringForDiagnostics(settings) ?? "",
2948 "claude.query.extra_args_json": encodeJsonStringForDiagnostics(extraArgs) ?? "",
29502949 "claude.query.path_to_executable": claudeBinaryPath,
29512950 });
29522951
apps/server/src/provider/Layers/CodexAdapter.test.ts+8-7
......@@ -46,6 +46,7 @@ import {
4646 type CodexThreadSnapshot,
4747} from "./CodexSessionRuntime.ts";
4848import { makeCodexAdapter } from "./CodexAdapter.ts";
49const decodeCodexSettings = Schema.decodeSync(CodexSettings);
4950
5051// Test-local service tag so the rest of the file can keep using `yield* CodexAdapter`.
5152class CodexAdapter extends Context.Service<CodexAdapter, CodexAdapterShape>()(
......@@ -224,7 +225,7 @@ const validationLayer = it.layer(
224225 Layer.effect(
225226 CodexAdapter,
226227 Effect.gen(function* () {
227 const codexConfig = Schema.decodeSync(CodexSettings)({});
228 const codexConfig = decodeCodexSettings({});
228229 return yield* makeCodexAdapter(codexConfig, {
229230 makeRuntime: validationRuntimeFactory.factory,
230231 });
......@@ -293,7 +294,7 @@ const sessionErrorLayer = it.layer(
293294 Layer.effect(
294295 CodexAdapter,
295296 Effect.gen(function* () {
296 const codexConfig = Schema.decodeSync(CodexSettings)({});
297 const codexConfig = decodeCodexSettings({});
297298 return yield* makeCodexAdapter(codexConfig, {
298299 makeRuntime: sessionRuntimeFactory.factory,
299300 });
......@@ -364,7 +365,7 @@ sessionErrorLayer("CodexAdapterLive session errors", (it) => {
364365 const customLayer = Layer.effect(
365366 CodexAdapter,
366367 Effect.gen(function* () {
367 const codexConfig = Schema.decodeSync(CodexSettings)({});
368 const codexConfig = decodeCodexSettings({});
368369 return yield* makeCodexAdapter(codexConfig, {
369370 instanceId: customInstanceId,
370371 makeRuntime: customRuntimeFactory.factory,
......@@ -419,7 +420,7 @@ const lifecycleLayer = it.layer(
419420 Layer.effect(
420421 CodexAdapter,
421422 Effect.gen(function* () {
422 const codexConfig = Schema.decodeSync(CodexSettings)({});
423 const codexConfig = decodeCodexSettings({});
423424 return yield* makeCodexAdapter(codexConfig, {
424425 makeRuntime: lifecycleRuntimeFactory.factory,
425426 });
......@@ -1034,7 +1035,7 @@ const scopedLifecycleLayer = it.layer(
10341035 Layer.effect(
10351036 CodexAdapter,
10361037 Effect.gen(function* () {
1037 const codexConfig = Schema.decodeSync(CodexSettings)({});
1038 const codexConfig = decodeCodexSettings({});
10381039 return yield* makeCodexAdapter(codexConfig, {
10391040 makeRuntime: scopedLifecycleRuntimeFactory.factory,
10401041 });
......@@ -1078,7 +1079,7 @@ const scopedFailureLayer = it.layer(
10781079 Layer.effect(
10791080 CodexAdapter,
10801081 Effect.gen(function* () {
1081 const codexConfig = Schema.decodeSync(CodexSettings)({});
1082 const codexConfig = decodeCodexSettings({});
10821083 return yield* makeCodexAdapter(codexConfig, {
10831084 makeRuntime: scopedFailureRuntimeFactory.factory,
10841085 });
......@@ -1127,7 +1128,7 @@ it.effect("flushes managed native logs when the adapter layer shuts down", () =>
11271128 const layer = Layer.effect(
11281129 CodexAdapter,
11291130 Effect.gen(function* () {
1130 const codexConfig = Schema.decodeSync(CodexSettings)({});
1131 const codexConfig = decodeCodexSettings({});
11311132 return yield* makeCodexAdapter(codexConfig, {
11321133 makeRuntime: runtimeFactory.factory,
11331134 nativeEventLogPath: basePath,
apps/server/src/provider/Layers/CodexAdapter.ts+11-7
......@@ -61,6 +61,12 @@ import {
6161 type CodexSessionRuntimeShape,
6262} from "./CodexSessionRuntime.ts";
6363import { type EventNdjsonLogger, makeEventNdjsonLogger } from "./EventNdjsonLogger.ts";
64const isCodexAppServerProcessExitedError = Schema.is(CodexErrors.CodexAppServerProcessExitedError);
65const isCodexAppServerTransportError = Schema.is(CodexErrors.CodexAppServerTransportError);
66const isCodexSessionRuntimeThreadIdMissingError = Schema.is(
67 CodexSessionRuntimeThreadIdMissingError,
68);
69const isCodexResumeCursorSchema = Schema.is(CodexResumeCursorSchema);
6470
6571const PROVIDER = ProviderDriverKind.make("codex");
6672
......@@ -91,10 +97,7 @@ function mapCodexRuntimeError(
9197 method: string,
9298 error: CodexSessionRuntimeError,
9399): ProviderAdapterError {
94 if (
95 Schema.is(CodexErrors.CodexAppServerProcessExitedError)(error) ||
96 Schema.is(CodexErrors.CodexAppServerTransportError)(error)
97 ) {
100 if (isCodexAppServerProcessExitedError(error) || isCodexAppServerTransportError(error)) {
98101 return new ProviderAdapterSessionClosedError({
99102 provider: PROVIDER,
100103 threadId,
......@@ -102,7 +105,7 @@ function mapCodexRuntimeError(
102105 });
103106 }
104107
105 if (Schema.is(CodexSessionRuntimeThreadIdMissingError)(error)) {
108 if (isCodexSessionRuntimeThreadIdMissingError(error)) {
106109 return new ProviderAdapterSessionNotFoundError({
107110 provider: PROVIDER,
108111 threadId,
......@@ -134,7 +137,8 @@ function readPayload<A>(
134137 schema: Schema.Schema<A>,
135138 payload: ProviderEvent["payload"],
136139): A | undefined {
137 return Schema.is(schema)(payload) ? payload : undefined;
140 const isPayload = Schema.is(schema);
141 return isPayload(payload) ? payload : undefined;
138142}
139143
140144function trimText(value: string | undefined | null): string | undefined {
......@@ -1381,7 +1385,7 @@ export const makeCodexAdapter = Effect.fn("makeCodexAdapter")(function* (
13811385 binaryPath: codexConfig.binaryPath,
13821386 ...(options?.environment ? { environment: options.environment } : {}),
13831387 ...(codexConfig.homePath ? { homePath: codexConfig.homePath } : {}),
1384 ...(Schema.is(CodexResumeCursorSchema)(input.resumeCursor)
1388 ...(isCodexResumeCursorSchema(input.resumeCursor)
13851389 ? { resumeCursor: input.resumeCursor }
13861390 : {}),
13871391 runtimeMode: input.runtimeMode,
apps/server/src/provider/Layers/CodexProvider.ts+2-1
......@@ -26,6 +26,7 @@ import { buildServerProvider, type ServerProviderDraft } from "../providerSnapsh
2626import { expandHomePath } from "../../pathExpansion.ts";
2727import { scopedSafeTeardown } from "./scopedSafeTeardown.ts";
2828import packageJson from "../../../package.json" with { type: "json" };
29const isCodexAppServerSpawnError = Schema.is(CodexErrors.CodexAppServerSpawnError);
2930
3031const PROVIDER_PROBE_TIMEOUT_MS = 8_000;
3132const CODEX_PRESENTATION = {
......@@ -450,7 +451,7 @@ export const checkCodexProviderStatus = Effect.fn("checkCodexProviderStatus")(fu
450451
451452 if (Result.isFailure(probeResult)) {
452453 const error = probeResult.failure;
453 const installed = !Schema.is(CodexErrors.CodexAppServerSpawnError)(error);
454 const installed = !isCodexAppServerSpawnError(error);
454455 return buildServerProvider({
455456 presentation: CODEX_PRESENTATION,
456457 enabled: codexSettings.enabled,
apps/server/src/provider/Layers/CodexSessionRuntime.test.ts+2-1
......@@ -16,6 +16,7 @@ import {
1616 isRecoverableThreadResumeError,
1717 openCodexThread,
1818} from "./CodexSessionRuntime.ts";
19const isCodexAppServerRequestError = Schema.is(CodexErrors.CodexAppServerRequestError);
1920
2021function makeThreadOpenResponse(
2122 threadId: string,
......@@ -269,7 +270,7 @@ describe("openCodexThread", () => {
269270 }),
270271 ),
271272 (error: unknown) =>
272 Schema.is(CodexErrors.CodexAppServerRequestError)(error) &&
273 isCodexAppServerRequestError(error) &&
273274 error.errorMessage === "timed out waiting for server",
274275 );
275276 });
apps/server/src/provider/Layers/CodexSessionRuntime.ts+10-6
......@@ -41,6 +41,7 @@ import {
4141 CODEX_DEFAULT_MODE_DEVELOPER_INSTRUCTIONS,
4242 CODEX_PLAN_MODE_DEVELOPER_INSTRUCTIONS,
4343} from "../CodexDeveloperInstructions.ts";
44const decodeV2TurnStartResponse = Schema.decodeUnknownEffect(EffectCodexSchema.V2TurnStartResponse);
4445
4546const PROVIDER = ProviderDriverKind.make("codex");
4647
......@@ -66,6 +67,8 @@ export const CodexResumeCursorSchema = Schema.Struct({
6667const CodexUserInputAnswerObject = Schema.Struct({
6768 answers: Schema.Array(Schema.String),
6869});
70const isCodexResumeCursorSchema = Schema.is(CodexResumeCursorSchema);
71const isCodexUserInputAnswerObject = Schema.is(CodexUserInputAnswerObject);
6972
7073// TODO: Verify `packages/effect-codex-app-server/scripts/generate.ts` so the generated
7174// `V2TurnStartParams` schema includes `collaborationMode` directly.
......@@ -74,6 +77,9 @@ const CodexTurnStartParamsWithCollaborationMode = EffectCodexSchema.V2TurnStartP
7477 collaborationMode: Schema.optionalKey(EffectCodexSchema.V2TurnStartParams__CollaborationMode),
7578 }),
7679);
80const decodeCodexTurnStartParamsWithCollaborationMode = Schema.decodeUnknownEffect(
81 CodexTurnStartParamsWithCollaborationMode,
82);
7783
7884export type CodexTurnStartParamsWithCollaborationMode =
7985 typeof CodexTurnStartParamsWithCollaborationMode.Type;
......@@ -248,7 +254,7 @@ function normalizeCodexModelSlug(
248254function readResumeCursorThreadId(
249255 resumeCursor: ProviderSession["resumeCursor"],
250256): string | undefined {
251 return Schema.is(CodexResumeCursorSchema)(resumeCursor) ? resumeCursor.threadId : undefined;
257 return isCodexResumeCursorSchema(resumeCursor) ? resumeCursor.threadId : undefined;
252258}
253259
254260function runtimeModeToThreadConfig(input: RuntimeMode): {
......@@ -367,7 +373,7 @@ export function buildTurnStartParams(input: {
367373 ...(input.effort ? { effort: input.effort } : {}),
368374 });
369375
370 return Schema.decodeUnknownEffect(CodexTurnStartParamsWithCollaborationMode)({
376 return decodeCodexTurnStartParamsWithCollaborationMode({
371377 threadId: input.threadId,
372378 input: turnInput,
373379 approvalPolicy: config.approvalPolicy,
......@@ -623,7 +629,7 @@ function toCodexUserInputAnswer(
623629 const answers = value.filter((entry): entry is string => typeof entry === "string");
624630 return Effect.succeed({ answers });
625631 }
626 if (Schema.is(CodexUserInputAnswerObject)(value)) {
632 if (isCodexUserInputAnswerObject(value)) {
627633 return Effect.succeed({ answers: value.answers });
628634 }
629635 return Effect.fail(new CodexSessionRuntimeInvalidUserInputAnswersError({ questionId }));
......@@ -1247,9 +1253,7 @@ export const makeCodexSessionRuntime = (
12471253 ...(input.interactionMode ? { interactionMode: input.interactionMode } : {}),
12481254 });
12491255 const rawResponse = yield* client.raw.request("turn/start", params);
1250 const response = yield* Schema.decodeUnknownEffect(EffectCodexSchema.V2TurnStartResponse)(
1251 rawResponse,
1252 ).pipe(
1256 const response = yield* decodeV2TurnStartResponse(rawResponse).pipe(
12531257 Effect.mapError((error) =>
12541258 toProtocolParseError("Invalid turn/start response payload", error),
12551259 ),
apps/server/src/provider/Layers/CursorAdapter.test.ts+4-3
......@@ -28,6 +28,7 @@ import { ServerConfig } from "../../config.ts";
2828import { ServerSettingsService } from "../../serverSettings.ts";
2929import type { CursorAdapterShape } from "../Services/CursorAdapter.ts";
3030import { makeCursorAdapter } from "./CursorAdapter.ts";
31const decodeCursorSettings = Schema.decodeSync(CursorSettings);
3132
3233// Test-local service tag so the rest of the file can keep using `yield* CursorAdapter`.
3334class CursorAdapter extends Context.Service<CursorAdapter, CursorAdapterShape>()(
......@@ -132,7 +133,7 @@ const cursorAdapterTestLayer = it.layer(
132133 Layer.effect(
133134 CursorAdapter,
134135 Effect.gen(function* () {
135 const cursorConfig = Schema.decodeSync(CursorSettings)({});
136 const cursorConfig = decodeCursorSettings({});
136137 const resolveSettings = yield* makeResolveCursorSettings;
137138 return yield* makeCursorAdapter(cursorConfig, { resolveSettings });
138139 }),
......@@ -607,7 +608,7 @@ cursorAdapterTestLayer("CursorAdapterLive", (it) => {
607608 Layer.effect(
608609 CursorAdapter,
609610 Effect.gen(function* () {
610 const cursorConfig = Schema.decodeSync(CursorSettings)({});
611 const cursorConfig = decodeCursorSettings({});
611612 const resolveSettings = yield* makeResolveCursorSettings;
612613 return yield* makeCursorAdapter(cursorConfig, { resolveSettings });
613614 }),
......@@ -1239,7 +1240,7 @@ cursorAdapterTestLayer("CursorAdapterLive", (it) => {
12391240 const customAdapterLayer = Layer.effect(
12401241 CursorAdapter,
12411242 Effect.gen(function* () {
1242 const cursorConfig = Schema.decodeSync(CursorSettings)({});
1243 const cursorConfig = decodeCursorSettings({});
12431244 const resolveSettings = yield* makeResolveCursorSettings;
12441245 return yield* makeCursorAdapter(cursorConfig, {
12451246 instanceId: customInstanceId,
apps/server/src/provider/Layers/CursorAdapter.ts+9-2
......@@ -75,6 +75,7 @@ import {
7575import { type CursorAdapterShape } from "../Services/CursorAdapter.ts";
7676import { resolveCursorAcpBaseModelId } from "./CursorProvider.ts";
7777import { type EventNdjsonLogger, makeEventNdjsonLogger } from "./EventNdjsonLogger.ts";
78const encodeUnknownJsonStringExit = Schema.encodeUnknownExit(Schema.UnknownFromJsonString);
7879
7980const PROVIDER = ProviderDriverKind.make("cursor");
8081const CURSOR_RESUME_VERSION = 1 as const;
......@@ -82,6 +83,11 @@ const ACP_PLAN_MODE_ALIASES = ["plan", "architect"];
8283const ACP_IMPLEMENT_MODE_ALIASES = ["code", "agent", "default", "chat", "implement"];
8384const ACP_APPROVAL_MODE_ALIASES = ["ask"];
8485
86function encodeJsonStringForDiagnostics(input: unknown): string | undefined {
87 const result = encodeUnknownJsonStringExit(input);
88 return Exit.isSuccess(result) ? result.value : undefined;
89}
90
8591export interface CursorAdapterLiveOptions {
8692 readonly environment?: NodeJS.ProcessEnv;
8793 readonly nativeEventLogPath?: string;
......@@ -390,7 +396,7 @@ export function makeCursorAdapter(
390396 method: string,
391397 ) =>
392398 Effect.gen(function* () {
393 const fingerprint = `${ctx.activeTurnId ?? "no-turn"}:${Schema.encodeUnknownSync(Schema.UnknownFromJsonString)(payload)}`;
399 const fingerprint = `${ctx.activeTurnId ?? "no-turn"}:${encodeJsonStringForDiagnostics(payload) ?? "[unserializable payload]"}`;
394400 if (ctx.lastPlanFingerprint === fingerprint) {
395401 return;
396402 }
......@@ -640,7 +646,8 @@ export function makeCursorAdapter(
640646 permissionRequest,
641647 detail:
642648 permissionRequest.detail ??
643 Schema.encodeUnknownSync(Schema.UnknownFromJsonString)(params).slice(0, 2000),
649 encodeJsonStringForDiagnostics(params)?.slice(0, 2000) ??
650 "[unserializable params]",
644651 args: params,
645652 source: "acp.jsonrpc",
646653 method: "session/request_permission",
apps/server/src/provider/Layers/OpenCodeProvider.test.ts+2-1
......@@ -16,6 +16,7 @@ import {
1616} from "../opencodeRuntime.ts";
1717import { checkOpenCodeProviderStatus } from "./OpenCodeProvider.ts";
1818import type { OpenCodeInventory } from "../opencodeRuntime.ts";
19const decodeOpenCodeSettings = Schema.decodeSync(OpenCodeSettings);
1920
2021const DEFAULT_VERSION_STDOUT = "opencode 1.14.19\n";
2122
......@@ -106,7 +107,7 @@ const testLayer = Layer.succeed(OpenCodeRuntime, OpenCodeRuntimeTestDouble).pipe
106107);
107108
108109const makeOpenCodeSettings = (overrides?: Partial<OpenCodeSettings>): OpenCodeSettings =>
109 Schema.decodeSync(OpenCodeSettings)({
110 decodeOpenCodeSettings({
110111 enabled: true,
111112 binaryPath: "opencode",
112113 serverUrl: "",
apps/server/src/provider/Layers/ProviderRegistry.test.ts+6-5
......@@ -47,6 +47,7 @@ import type { ProviderInstance } from "../ProviderDriver.ts";
4747import { ProviderInstanceRegistry } from "../Services/ProviderInstanceRegistry.ts";
4848import { ProviderRegistry } from "../Services/ProviderRegistry.ts";
4949import { makeManualOnlyProviderMaintenanceCapabilities } from "../providerMaintenance.ts";
50const decodeServerSettings = Schema.decodeSync(ServerSettings);
5051
5152const defaultClaudeSettings: ClaudeSettings = Schema.decodeSync(ClaudeSettings)({});
5253const defaultCodexSettings: CodexSettings = Schema.decodeSync(CodexSettings)({});
......@@ -255,7 +256,7 @@ function makeMutableServerSettingsService(
255256 updateSettings: (patch) =>
256257 Effect.gen(function* () {
257258 const current = yield* Ref.get(settingsRef);
258 const next = Schema.decodeSync(ServerSettings)(deepMerge(current, patch));
259 const next = decodeServerSettings(deepMerge(current, patch));
259260 yield* Ref.set(settingsRef, next);
260261 yield* PubSub.publish(changes, next);
261262 return next;
......@@ -928,7 +929,7 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsService.layerTest(), T
928929 Effect.gen(function* () {
929930 const missingBinary = `t3code_codex_missing_`;
930931 const serverSettings = yield* makeMutableServerSettingsService(
931 Schema.decodeSync(ServerSettings)(
932 decodeServerSettings(
932933 deepMerge(DEFAULT_SERVER_SETTINGS, {
933934 providers: {
934935 // Disable every built-in probe that would otherwise spawn
......@@ -1027,7 +1028,7 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsService.layerTest(), T
10271028 const firstMissing = `t3code_codex_first_`;
10281029 const secondMissing = `t3code_codex_second_`;
10291030 const serverSettings = yield* makeMutableServerSettingsService(
1030 Schema.decodeSync(ServerSettings)(
1031 decodeServerSettings(
10311032 deepMerge(DEFAULT_SERVER_SETTINGS, {
10321033 providers: {
10331034 codex: { enabled: true, binaryPath: firstMissing },
......@@ -1122,7 +1123,7 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsService.layerTest(), T
11221123 it.effect("includes unavailable instance snapshots in getProviders", () =>
11231124 Effect.gen(function* () {
11241125 const serverSettings = yield* makeMutableServerSettingsService(
1125 Schema.decodeSync(ServerSettings)(
1126 decodeServerSettings(
11261127 deepMerge(DEFAULT_SERVER_SETTINGS, {
11271128 providers: {
11281129 codex: { enabled: false },
......@@ -1178,7 +1179,7 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsService.layerTest(), T
11781179 () =>
11791180 Effect.gen(function* () {
11801181 const serverSettings = yield* makeMutableServerSettingsService(
1181 Schema.decodeSync(ServerSettings)(
1182 decodeServerSettings(
11821183 deepMerge(DEFAULT_SERVER_SETTINGS, {
11831184 providers: {
11841185 codex: {
apps/server/src/provider/Layers/ProviderService.ts+6-3
......@@ -56,6 +56,7 @@ import {
5656import { type EventNdjsonLogger } from "./EventNdjsonLogger.ts";
5757import { ProviderEventLoggers } from "./ProviderEventLoggers.ts";
5858import { AnalyticsService } from "../../telemetry/Services/AnalyticsService.ts";
59const isModelSelection = Schema.is(ModelSelection);
5960
6061/**
6162 * Hook for tests that want to override the canonical event logger pulled
......@@ -87,8 +88,9 @@ const decodeInputOrValidationError = <S extends Schema.Top>(input: {
8788 readonly operation: string;
8889 readonly schema: S;
8990 readonly payload: unknown;
90}) =>
91 Schema.decodeUnknownEffect(input.schema)(input.payload).pipe(
91}) => {
92 const decodeProviderRequestInput = Schema.decodeUnknownEffect(input.schema);
93 return decodeProviderRequestInput(input.payload).pipe(
9294 Effect.mapError(
9395 (schemaError) =>
9496 new ProviderValidationError({
......@@ -98,6 +100,7 @@ const decodeInputOrValidationError = <S extends Schema.Top>(input: {
98100 }),
99101 ),
100102 );
103};
101104
102105function toRuntimeStatus(session: ProviderSession): "starting" | "running" | "stopped" | "error" {
103106 switch (session.status) {
......@@ -142,7 +145,7 @@ function readPersistedModelSelection(
142145 return undefined;
143146 }
144147 const raw = "modelSelection" in runtimePayload ? runtimePayload.modelSelection : undefined;
145 return Schema.is(ModelSelection)(raw) ? raw : undefined;
148 return isModelSelection(raw) ? raw : undefined;
146149}
147150
148151function readPersistedCwd(
apps/server/src/provider/Layers/ProviderSessionDirectory.ts+2-1
......@@ -14,6 +14,7 @@ import {
1414 type ProviderRuntimeBindingWithMetadata,
1515 type ProviderSessionDirectoryShape,
1616} from "../Services/ProviderSessionDirectory.ts";
17const decodeProviderDriverKindValue = Schema.decodeUnknownEffect(ProviderDriverKind);
1718
1819function toPersistenceError(operation: string) {
1920 return (cause: unknown) =>
......@@ -28,7 +29,7 @@ function decodeProviderDriverKind(
2829 providerName: string,
2930 operation: string,
3031): Effect.Effect<ProviderDriverKind, ProviderSessionDirectoryPersistenceError> {
31 return Schema.decodeUnknownEffect(ProviderDriverKind)(providerName).pipe(
32 return decodeProviderDriverKindValue(providerName).pipe(
3233 Effect.mapError(
3334 (cause) =>
3435 new ProviderSessionDirectoryPersistenceError({
apps/server/src/provider/acp/AcpAdapterSupport.ts+4-2
......@@ -11,6 +11,8 @@ import {
1111 ProviderAdapterSessionClosedError,
1212 type ProviderAdapterError,
1313} from "../Errors.ts";
14const isAcpProcessExitedError = Schema.is(EffectAcpErrors.AcpProcessExitedError);
15const isAcpRequestError = Schema.is(EffectAcpErrors.AcpRequestError);
1416
1517export function mapAcpToAdapterError(
1618 provider: ProviderDriverKind,
......@@ -18,14 +20,14 @@ export function mapAcpToAdapterError(
1820 method: string,
1921 error: EffectAcpErrors.AcpError,
2022): ProviderAdapterError {
21 if (Schema.is(EffectAcpErrors.AcpProcessExitedError)(error)) {
23 if (isAcpProcessExitedError(error)) {
2224 return new ProviderAdapterSessionClosedError({
2325 provider,
2426 threadId,
2527 cause: error,
2628 });
2729 }
28 if (Schema.is(EffectAcpErrors.AcpRequestError)(error)) {
30 if (isAcpRequestError(error)) {
2931 return new ProviderAdapterRequestError({
3032 provider,
3133 method,
apps/server/src/provider/acp/AcpSessionRuntime.ts+7-4
......@@ -7,7 +7,6 @@ import * as Queue from "effect/Queue";
77import * as Ref from "effect/Ref";
88import * as Scope from "effect/Scope";
99import * as Context from "effect/Context";
10import * as Schema from "effect/Schema";
1110import * as Stream from "effect/Stream";
1211import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";
1312import * as EffectAcpClient from "effect-acp/client";
......@@ -27,6 +26,10 @@ import {
2726 type AcpToolCallState,
2827} from "./AcpRuntimeModel.ts";
2928
29function formatConfigOptionValue(value: string | boolean): string {
30 return JSON.stringify(value);
31}
32
3033export interface AcpSpawnInput {
3134 readonly command: string;
3235 readonly args: ReadonlyArray<string>;
......@@ -277,7 +280,7 @@ const makeAcpSessionRuntime = (
277280 }
278281 return yield* new EffectAcpErrors.AcpRequestError({
279282 code: -32602,
280 errorMessage: `Invalid value ${Schema.encodeUnknownSync(Schema.UnknownFromJsonString)(value)} for session config option "${configOption.id}": expected boolean`,
283 errorMessage: `Invalid value ${formatConfigOptionValue(value)} for session config option "${configOption.id}": expected boolean`,
281284 data: {
282285 configId: configOption.id,
283286 expectedType: "boolean",
......@@ -288,7 +291,7 @@ const makeAcpSessionRuntime = (
288291 if (typeof value !== "string") {
289292 return yield* new EffectAcpErrors.AcpRequestError({
290293 code: -32602,
291 errorMessage: `Invalid value ${Schema.encodeUnknownSync(Schema.UnknownFromJsonString)(value)} for session config option "${configOption.id}": expected string`,
294 errorMessage: `Invalid value ${formatConfigOptionValue(value)} for session config option "${configOption.id}": expected string`,
292295 data: {
293296 configId: configOption.id,
294297 expectedType: "string",
......@@ -302,7 +305,7 @@ const makeAcpSessionRuntime = (
302305 }
303306 return yield* new EffectAcpErrors.AcpRequestError({
304307 code: -32602,
305 errorMessage: `Invalid value ${Schema.encodeUnknownSync(Schema.UnknownFromJsonString)(value)} for session config option "${configOption.id}": expected one of ${allowedValues.join(", ")}`,
308 errorMessage: `Invalid value ${formatConfigOptionValue(value)} for session config option "${configOption.id}": expected one of ${allowedValues.join(", ")}`,
306309 data: {
307310 configId: configOption.id,
308311 allowedValues,
apps/server/src/provider/opencodeRuntime.ts+11-5
......@@ -31,6 +31,8 @@ import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";
3131import { isWindowsCommandNotFound } from "../processRunner.ts";
3232import { collectStreamAsString } from "./providerSnapshot.ts";
3333import * as NetService from "@t3tools/shared/Net";
34const encodeUnknownJsonStringExit = Schema.encodeUnknownExit(Schema.UnknownFromJsonString);
35const OPENCODE_EMPTY_CONFIG_CONTENT = "{}";
3436
3537const OPENCODE_SERVER_READY_PREFIX = "opencode server listening";
3638const DEFAULT_OPENCODE_SERVER_TIMEOUT_MS = 5_000;
......@@ -56,6 +58,11 @@ export class OpenCodeRuntimeError extends Data.TaggedError(OPENCODE_RUNTIME_ERRO
5658 P.isTagged(u, OPENCODE_RUNTIME_ERROR_TAG);
5759}
5860
61function encodeJsonStringForDiagnostics(input: unknown): string | undefined {
62 const result = encodeUnknownJsonStringExit(input);
63 return Exit.isSuccess(result) ? result.value : undefined;
64}
65
5966export function openCodeRuntimeErrorDetail(cause: unknown): string {
6067 if (OpenCodeRuntimeError.is(cause)) return cause.detail;
6168 if (cause instanceof Error && cause.message.trim().length > 0) return cause.message.trim();
......@@ -64,10 +71,9 @@ export function openCodeRuntimeErrorDetail(cause: unknown): string {
6471 const anyCause = cause as Record<string, unknown>;
6572 const status = (anyCause.response as { status?: number } | undefined)?.status;
6673 const body = anyCause.error ?? anyCause.data ?? anyCause.body;
67 try {
68 return `status=${status ?? "?"} body=${Schema.encodeUnknownSync(Schema.UnknownFromJsonString)(body ?? cause)}`;
69 } catch {
70 /* fall through */
74 const encodedBody = encodeJsonStringForDiagnostics(body ?? cause);
75 if (encodedBody) {
76 return `status=${status ?? "?"} body=${encodedBody}`;
7177 }
7278 }
7379 return String(cause);
......@@ -336,7 +342,7 @@ const makeOpenCodeRuntime = Effect.gen(function* () {
336342 shell: process.platform === "win32",
337343 env: {
338344 ...(input.environment ?? process.env),
339 OPENCODE_CONFIG_CONTENT: Schema.encodeUnknownSync(Schema.UnknownFromJsonString)({}),
345 OPENCODE_CONFIG_CONTENT: OPENCODE_EMPTY_CONFIG_CONTENT,
340346 },
341347 }),
342348 )
apps/server/src/provider/providerMaintenanceRunner.test.ts+3-2
......@@ -25,6 +25,7 @@ import {
2525 makeProviderMaintenanceCapabilities,
2626 type ProviderMaintenanceCapabilities,
2727} from "./providerMaintenance.ts";
28const isServerProviderUpdateError = Schema.is(ServerProviderUpdateError);
2829
2930const CODEX_DRIVER = ProviderDriverKind.make("codex");
3031const CURSOR_DRIVER = ProviderDriverKind.make("cursor");
......@@ -455,8 +456,8 @@ describe("providerMaintenanceRunner", () => {
455456 assert.strictEqual(Exit.isFailure(second), true);
456457 if (Exit.isFailure(second)) {
457458 const error = Cause.squash(second.cause);
458 assert.strictEqual(Schema.is(ServerProviderUpdateError)(error), true);
459 if (Schema.is(ServerProviderUpdateError)(error)) {
459 assert.strictEqual(isServerProviderUpdateError(error), true);
460 if (isServerProviderUpdateError(error)) {
460461 assert.include(error.reason, "already running");
461462 }
462463 }
apps/server/src/provider/providerMaintenanceRunner.ts+2-1
......@@ -25,6 +25,7 @@ import { makeProviderMaintenanceCommandCoordinator } from "./providerMaintenance
2525import { enrichProviderSnapshotWithVersionAdvisory } from "./providerMaintenance.ts";
2626import type { ProviderMaintenanceCapabilities } from "./providerMaintenance.ts";
2727import { collectUint8StreamText } from "../stream/collectUint8StreamText.ts";
28const isServerProviderUpdateError = Schema.is(ServerProviderUpdateError);
2829
2930const UPDATE_TIMEOUT_MS = 5 * 60_000;
3031const UPDATE_OUTPUT_MAX_BYTES = 10_000;
......@@ -397,7 +398,7 @@ export const make = Effect.fn("ProviderMaintenanceRunner.make")(function* () {
397398 })
398399 .pipe(
399400 Effect.mapError((error) =>
400 Schema.is(ServerProviderUpdateError)(error)
401 isServerProviderUpdateError(error)
401402 ? new ServerProviderUpdateError({
402403 provider,
403404 reason: error.reason,
apps/server/src/serverSettings.ts+5-5
......@@ -49,6 +49,7 @@ import { fromLenientJson } from "@t3tools/shared/schemaJson";
4949import { applyServerSettingsPatch } from "@t3tools/shared/serverSettings";
5050import { ServerSecretStoreLive } from "./auth/Layers/ServerSecretStore.ts";
5151import { ServerSecretStore } from "./auth/Services/ServerSecretStore.ts";
52const decodeServerSettings = Schema.decodeEffect(ServerSettings);
5253
5354const textEncoder = new TextEncoder();
5455const textDecoder = new TextDecoder();
......@@ -127,9 +128,7 @@ export class ServerSettingsService extends Context.Service<
127128 updateSettings: (patch) =>
128129 Ref.get(currentSettingsRef).pipe(
129130 Effect.flatMap((currentSettings) =>
130 Schema.decodeEffect(ServerSettings)(
131 applyServerSettingsPatch(currentSettings, patch),
132 ).pipe(
131 decodeServerSettings(applyServerSettingsPatch(currentSettings, patch)).pipe(
133132 Effect.mapError(
134133 (cause) =>
135134 new ServerSettingsError({
......@@ -149,6 +148,7 @@ export class ServerSettingsService extends Context.Service<
149148}
150149
151150const ServerSettingsJson = fromLenientJson(ServerSettings);
151const decodeServerSettingsJsonExit = Schema.decodeUnknownExit(ServerSettingsJson);
152152
153153type LegacyProviderSettings = ServerSettings["providers"][keyof ServerSettings["providers"]];
154154
......@@ -280,7 +280,7 @@ const makeServerSettings = Effect.gen(function* () {
280280 }
281281
282282 const raw = yield* readRawConfig;
283 const decoded = Schema.decodeUnknownExit(ServerSettingsJson)(raw);
283 const decoded = decodeServerSettingsJsonExit(raw);
284284 if (decoded._tag === "Failure") {
285285 yield* Effect.logWarning("failed to parse settings.json, using defaults", {
286286 path: settingsPath,
......@@ -533,7 +533,7 @@ const makeServerSettings = Effect.gen(function* () {
533533 current,
534534 applyServerSettingsPatch(current, patch),
535535 );
536 const next = yield* Schema.decodeEffect(ServerSettings)(nextPersisted).pipe(
536 const next = yield* decodeServerSettings(nextPersisted).pipe(
537537 Effect.mapError(
538538 (cause) =>
539539 new ServerSettingsError({
apps/server/src/sourceControl/BitbucketApi.ts+2-1
......@@ -44,6 +44,7 @@ export class BitbucketApiError extends Schema.TaggedErrorClass<BitbucketApiError
4444 return `Bitbucket API failed in ${this.operation}: ${this.detail}`;
4545 }
4646}
47const isBitbucketApiErrorValue = Schema.is(BitbucketApiError);
4748
4849const RawBitbucketRepositorySchema = Schema.Struct({
4950 full_name: TrimmedNonEmptyString,
......@@ -350,7 +351,7 @@ function requestError(operation: string, cause: unknown): BitbucketApiError {
350351}
351352
352353function isBitbucketApiError(cause: unknown): cause is BitbucketApiError {
353 return Schema.is(BitbucketApiError)(cause);
354 return isBitbucketApiErrorValue(cause);
354355}
355356
356357function responseError(
apps/server/src/sourceControl/GitHubSourceControlProvider.ts+2-1
......@@ -13,6 +13,7 @@ import * as GitHubCli from "./GitHubCli.ts";
1313import * as GitHubPullRequests from "./gitHubPullRequests.ts";
1414import * as SourceControlProvider from "./SourceControlProvider.ts";
1515import * as SourceControlProviderDiscovery from "./SourceControlProviderDiscovery.ts";
16const isSourceControlProviderError = Schema.is(SourceControlProviderError);
1617
1718function providerError(
1819 operation: string,
......@@ -153,7 +154,7 @@ export const make = Effect.fn("makeGitHubSourceControlProvider")(function* () {
153154 );
154155 }),
155156 Effect.mapError((error) =>
156 Schema.is(SourceControlProviderError)(error)
157 isSourceControlProviderError(error)
157158 ? error
158159 : providerError("listChangeRequests", error),
159160 ),
apps/server/src/sourceControl/SourceControlRepositoryService.ts+2-1
......@@ -22,6 +22,7 @@ import {
2222import { ServerConfig } from "../config.ts";
2323import * as GitVcsDriver from "../vcs/GitVcsDriver.ts";
2424import * as SourceControlProviderRegistry from "./SourceControlProviderRegistry.ts";
25const isSourceControlRepositoryError = Schema.is(SourceControlRepositoryError);
2526
2627export interface SourceControlRepositoryServiceShape {
2728 readonly lookupRepository: (
......@@ -69,7 +70,7 @@ function repositoryError(input: {
6970
7071function mapRepositoryError(operation: string, provider: SourceControlProviderKind) {
7172 return Effect.mapError((cause: unknown) =>
72 Schema.is(SourceControlRepositoryError)(cause)
73 isSourceControlRepositoryError(cause)
7374 ? cause
7475 : repositoryError({
7576 operation,
apps/server/src/textGeneration/ClaudeTextGeneration.test.ts+2-1
......@@ -13,6 +13,7 @@ import { ServerConfig } from "../config.ts";
1313import { type TextGenerationShape } from "./TextGeneration.ts";
1414import { sanitizeThreadTitle } from "./TextGenerationUtils.ts";
1515import { makeClaudeTextGeneration } from "./ClaudeTextGeneration.ts";
16const decodeClaudeSettings = Schema.decodeSync(ClaudeSettings);
1617
1718const ClaudeTextGenerationTestLayer = ServerConfig.layerTest(process.cwd(), {
1819 prefix: "t3code-claude-text-generation-test-",
......@@ -182,7 +183,7 @@ function withFakeClaudeEnv<A, E, R>(
182183 }),
183184 );
184185
185 const config = Schema.decodeSync(ClaudeSettings)(input.claudeConfig ?? {});
186 const config = decodeClaudeSettings(input.claudeConfig ?? {});
186187 const textGeneration = yield* makeClaudeTextGeneration(config);
187188 return yield* effectFn(textGeneration);
188189 }).pipe(Effect.scoped);
apps/server/src/textGeneration/ClaudeTextGeneration.ts+38-8
......@@ -53,6 +53,9 @@ const ClaudeOutputEnvelope = Schema.Struct({
5353 structured_output: Schema.Unknown,
5454});
5555
56const encodeJsonString = Schema.encodeEffect(Schema.UnknownFromJsonString);
57const decodeClaudeOutputEnvelope = Schema.decodeEffect(Schema.fromJsonString(ClaudeOutputEnvelope));
58
5659export const makeClaudeTextGeneration = Effect.fn("makeClaudeTextGeneration")(function* (
5760 claudeSettings: ClaudeSettings,
5861 environment: NodeJS.ProcessEnv = process.env,
......@@ -75,6 +78,26 @@ export const makeClaudeTextGeneration = Effect.fn("makeClaudeTextGeneration")(fu
7578 ),
7679 );
7780
81 const encodeJsonForOperation = (
82 operation:
83 | "generateCommitMessage"
84 | "generatePrContent"
85 | "generateBranchName"
86 | "generateThreadTitle",
87 value: unknown,
88 detail: string,
89 ): Effect.Effect<string, TextGenerationError> =>
90 encodeJsonString(value).pipe(
91 Effect.mapError(
92 (cause) =>
93 new TextGenerationError({
94 operation,
95 detail,
96 cause,
97 }),
98 ),
99 );
100
78101 /**
79102 * Spawn the Claude CLI with structured JSON output and return the parsed,
80103 * schema-validated result.
......@@ -96,8 +119,10 @@ export const makeClaudeTextGeneration = Effect.fn("makeClaudeTextGeneration")(fu
96119 outputSchemaJson: S;
97120 modelSelection: ModelSelection;
98121 }): Effect.fn.Return<S["Type"], TextGenerationError, S["DecodingServices"]> {
99 const jsonSchemaStr = Schema.encodeUnknownSync(Schema.UnknownFromJsonString)(
122 const jsonSchemaStr = yield* encodeJsonForOperation(
123 operation,
100124 toJsonSchemaObject(outputSchemaJson),
125 "Failed to encode structured output schema.",
101126 );
102127 const caps = getClaudeModelCapabilities(modelSelection.model);
103128 const descriptors = getProviderOptionDescriptors({
......@@ -118,6 +143,14 @@ export const makeClaudeTextGeneration = Effect.fn("makeClaudeTextGeneration")(fu
118143 ...(typeof thinking === "boolean" ? { alwaysThinkingEnabled: thinking } : {}),
119144 ...(fastMode ? { fastMode: true } : {}),
120145 };
146 const settingsJson =
147 Object.keys(settings).length > 0
148 ? yield* encodeJsonForOperation(
149 operation,
150 settings,
151 "Failed to encode Claude CLI settings.",
152 )
153 : undefined;
121154
122155 const runClaudeCommand = Effect.fn("runClaudeJson.runClaudeCommand")(function* () {
123156 const command = ChildProcess.make(
......@@ -131,9 +164,7 @@ export const makeClaudeTextGeneration = Effect.fn("makeClaudeTextGeneration")(fu
131164 "--model",
132165 resolveClaudeApiModelId(modelSelection),
133166 ...(cliEffort ? ["--effort", cliEffort] : []),
134 ...(Object.keys(settings).length > 0
135 ? ["--settings", Schema.encodeUnknownSync(Schema.UnknownFromJsonString)(settings)]
136 : []),
167 ...(settingsJson ? ["--settings", settingsJson] : []),
137168 "--dangerously-skip-permissions",
138169 ],
139170 {
......@@ -197,9 +228,7 @@ export const makeClaudeTextGeneration = Effect.fn("makeClaudeTextGeneration")(fu
197228 ),
198229 );
199230
200 const envelope = yield* Schema.decodeEffect(Schema.fromJsonString(ClaudeOutputEnvelope))(
201 rawStdout,
202 ).pipe(
231 const envelope = yield* decodeClaudeOutputEnvelope(rawStdout).pipe(
203232 Effect.catchTag("SchemaError", (cause) =>
204233 Effect.fail(
205234 new TextGenerationError({
......@@ -211,7 +240,8 @@ export const makeClaudeTextGeneration = Effect.fn("makeClaudeTextGeneration")(fu
211240 ),
212241 );
213242
214 return yield* Schema.decodeEffect(outputSchemaJson)(envelope.structured_output).pipe(
243 const decodeOutput = Schema.decodeEffect(outputSchemaJson);
244 return yield* decodeOutput(envelope.structured_output).pipe(
215245 Effect.catchTag("SchemaError", (cause) =>
216246 Effect.fail(
217247 new TextGenerationError({
apps/server/src/textGeneration/CodexTextGeneration.test.ts+2-1
......@@ -14,6 +14,7 @@ import { CodexSettings, ProviderInstanceId, TextGenerationError } from "@t3tools
1414import { ServerConfig } from "../config.ts";
1515import { type TextGenerationShape } from "./TextGeneration.ts";
1616import { makeCodexTextGeneration } from "./CodexTextGeneration.ts";
17const decodeCodexSettings = Schema.decodeSync(CodexSettings);
1718
1819const DEFAULT_TEST_MODEL_SELECTION = createModelSelection(
1920 ProviderInstanceId.make("codex"),
......@@ -172,7 +173,7 @@ function withFakeCodexEnv<A, E, R>(
172173 const fs = yield* FileSystem.FileSystem;
173174 const tempDir = yield* fs.makeTempDirectoryScoped({ prefix: "t3code-codex-text-" });
174175 const codexPath = yield* makeFakeCodexBinary(tempDir, input);
175 const config = Schema.decodeSync(CodexSettings)({ binaryPath: codexPath });
176 const config = decodeCodexSettings({ binaryPath: codexPath });
176177 const textGeneration = yield* makeCodexTextGeneration(config);
177178 return yield* effectFn(textGeneration);
178179 }).pipe(Effect.scoped);
apps/server/src/textGeneration/CodexTextGeneration.ts+26-4
......@@ -40,6 +40,7 @@ import {
4040
4141const CODEX_GIT_TEXT_GENERATION_REASONING_EFFORT = "low";
4242const CODEX_TIMEOUT_MS = 180_000;
43const encodeJsonString = Schema.encodeEffect(Schema.UnknownFromJsonString);
4344/**
4445 * Build a Codex text-generation closure bound to a specific `CodexSettings`
4546 * payload. See `makeCodexAdapter` for the overall per-instance rationale.
......@@ -99,6 +100,25 @@ export const makeCodexTextGeneration = Effect.fn("makeCodexTextGeneration")(func
99100 const safeUnlink = (filePath: string): Effect.Effect<void, never> =>
100101 fileSystem.remove(filePath).pipe(Effect.catch(() => Effect.void));
101102
103 const encodeJsonForOperation = (
104 operation:
105 | "generateCommitMessage"
106 | "generatePrContent"
107 | "generateBranchName"
108 | "generateThreadTitle",
109 value: unknown,
110 ): Effect.Effect<string, TextGenerationError> =>
111 encodeJsonString(value).pipe(
112 Effect.mapError(
113 (cause) =>
114 new TextGenerationError({
115 operation,
116 detail: "Failed to encode structured output schema.",
117 cause,
118 }),
119 ),
120 );
121
102122 const materializeImageAttachments = Effect.fn("materializeImageAttachments")(function* (
103123 _operation:
104124 | "generateCommitMessage"
......@@ -156,11 +176,11 @@ export const makeCodexTextGeneration = Effect.fn("makeCodexTextGeneration")(func
156176 cleanupPaths?: ReadonlyArray<string>;
157177 modelSelection: ModelSelection;
158178 }): Effect.fn.Return<S["Type"], TextGenerationError, S["DecodingServices"]> {
159 const schemaPath = yield* writeTempFile(
179 const schemaJson = yield* encodeJsonForOperation(
160180 operation,
161 "codex-schema",
162 Schema.encodeUnknownSync(Schema.UnknownFromJsonString)(toJsonSchemaObject(outputSchemaJson)),
181 toJsonSchemaObject(outputSchemaJson),
163182 );
183 const schemaPath = yield* writeTempFile(operation, "codex-schema", schemaJson);
164184 const outputPath = yield* writeTempFile(operation, "codex-output", "");
165185
166186 const runCodexCommand = Effect.fn("runCodexJson.runCodexCommand")(function* () {
......@@ -259,6 +279,8 @@ export const makeCodexTextGeneration = Effect.fn("makeCodexTextGeneration")(func
259279 ),
260280 );
261281
282 const decodeOutput = Schema.decodeEffect(Schema.fromJsonString(outputSchemaJson));
283
262284 return yield* fileSystem.readFileString(outputPath).pipe(
263285 Effect.mapError(
264286 (cause) =>
......@@ -268,7 +290,7 @@ export const makeCodexTextGeneration = Effect.fn("makeCodexTextGeneration")(func
268290 cause,
269291 }),
270292 ),
271 Effect.flatMap(Schema.decodeEffect(Schema.fromJsonString(outputSchemaJson))),
293 Effect.flatMap(decodeOutput),
272294 Effect.catchTag("SchemaError", (cause) =>
273295 Effect.fail(
274296 new TextGenerationError({
apps/server/src/textGeneration/CursorTextGeneration.test.ts+2-1
......@@ -19,6 +19,7 @@ import { CursorSettings, ProviderInstanceId } from "@t3tools/contracts";
1919import { ServerConfig } from "../config.ts";
2020import { type TextGenerationShape } from "./TextGeneration.ts";
2121import { makeCursorTextGeneration } from "./CursorTextGeneration.ts";
22const decodeCursorSettings = Schema.decodeSync(CursorSettings);
2223
2324const __dirname = path.dirname(fileURLToPath(import.meta.url));
2425const mockAgentPath = path.join(__dirname, "../../scripts/acp-mock-agent.ts");
......@@ -65,7 +66,7 @@ function withFakeAcpAgent<A, E, R>(
6566 }),
6667 );
6768 const agentPath = makeAcpAgentWrapper(tempDir, env);
68 const config = Schema.decodeSync(CursorSettings)({ binaryPath: agentPath });
69 const config = decodeCursorSettings({ binaryPath: agentPath });
6970 const textGeneration = yield* makeCursorTextGeneration(config);
7071 return yield* effectFn(textGeneration);
7172 }).pipe(Effect.scoped);
apps/server/src/textGeneration/CursorTextGeneration.ts+2-3
......@@ -154,9 +154,8 @@ export const makeCursorTextGeneration = Effect.fn("makeCursorTextGeneration")(fu
154154 });
155155 }
156156
157 return yield* Schema.decodeEffect(Schema.fromJsonString(outputSchemaJson))(
158 extractJsonObject(rawResult),
159 ).pipe(
157 const decodeOutput = Schema.decodeEffect(Schema.fromJsonString(outputSchemaJson));
158 return yield* decodeOutput(extractJsonObject(rawResult)).pipe(
160159 Effect.catchTag("SchemaError", (cause) =>
161160 Effect.fail(
162161 new TextGenerationError({
apps/server/src/textGeneration/OpenCodeTextGeneration.ts+2-3
......@@ -352,9 +352,8 @@ export const makeOpenCodeTextGeneration = Effect.fn("makeOpenCodeTextGeneration"
352352 releaseSharedServer,
353353 );
354354
355 return yield* Schema.decodeEffect(Schema.fromJsonString(input.outputSchemaJson))(
356 extractJsonObject(rawOutput),
357 ).pipe(
355 const decodeOutput = Schema.decodeEffect(Schema.fromJsonString(input.outputSchemaJson));
356 return yield* decodeOutput(extractJsonObject(rawOutput)).pipe(
358357 Effect.catchTag("SchemaError", (cause) =>
359358 Effect.fail(
360359 new TextGenerationError({
apps/server/src/textGeneration/TextGenerationUtils.ts+2-1
......@@ -1,6 +1,7 @@
11import * as Schema from "effect/Schema";
22
33import { TextGenerationError } from "@t3tools/contracts";
4const isTextGenerationError = Schema.is(TextGenerationError);
45
56/** Convert an Effect Schema to a flat JSON Schema object, inlining `$defs` when present. */
67export function toJsonSchemaObject(schema: Schema.Top): unknown {
......@@ -127,7 +128,7 @@ export function normalizeCliError(
127128 error: unknown,
128129 fallback: string,
129130): TextGenerationError {
130 if (Schema.is(TextGenerationError)(error)) {
131 if (isTextGenerationError(error)) {
131132 return error;
132133 }
133134
apps/server/src/vcs/GitVcsDriverCore.ts+2-1
......@@ -28,6 +28,7 @@ import {
2828 parseRemoteRefWithRemoteNames,
2929} from "../git/remoteRefs.ts";
3030import { ServerConfig } from "../config.ts";
31const isGitCommandError = Schema.is(GitCommandError);
3132
3233const DEFAULT_TIMEOUT_MS = 30_000;
3334const DEFAULT_MAX_OUTPUT_BYTES = 1_000_000;
......@@ -327,7 +328,7 @@ function toGitCommandError(
327328 detail: string,
328329) {
329330 return (cause: unknown) =>
330 Schema.is(GitCommandError)(cause)
331 isGitCommandError(cause)
331332 ? cause
332333 : new GitCommandError({
333334 operation: input.operation,
apps/server/src/vcs/VcsProjectConfig.ts+2-1
......@@ -15,6 +15,7 @@ const ProjectVcsConfig = Schema.Struct({
1515 ),
1616 vcsKind: Schema.optional(VcsDriverKind),
1717});
18const isProjectVcsConfig = Schema.is(ProjectVcsConfig);
1819
1920interface ProjectVcsConfigFile {
2021 readonly vcs?:
......@@ -47,7 +48,7 @@ function configuredKind(config: ProjectVcsConfigFile): VcsDriverKindType | "auto
4748function parseConfig(raw: string): ProjectVcsConfigFile | null {
4849 try {
4950 const parsed = JSON.parse(raw) as unknown;
50 return Schema.is(ProjectVcsConfig)(parsed) ? parsed : null;
51 return isProjectVcsConfig(parsed) ? parsed : null;
5152 } catch {
5253 return null;
5354 }
apps/server/src/ws.ts+6-4
......@@ -86,6 +86,8 @@ import {
8686 type SessionCredentialChange,
8787} from "./auth/Services/SessionCredentialService.ts";
8888import { respondToAuthError } from "./auth/http.ts";
89const isOrchestrationDispatchCommandError = Schema.is(OrchestrationDispatchCommandError);
90const isWorkspacePathOutsideRootError = Schema.is(WorkspacePathOutsideRootError);
8991
9092const nowIso = Effect.map(DateTime.now, DateTime.formatIso);
9193
......@@ -216,7 +218,7 @@ const makeWsRpcLayer = (currentSessionId: AuthSessionId) =>
216218 });
217219
218220 const toDispatchCommandError = (cause: unknown, fallbackMessage: string) =>
219 Schema.is(OrchestrationDispatchCommandError)(cause)
221 isOrchestrationDispatchCommandError(cause)
220222 ? cause
221223 : new OrchestrationDispatchCommandError({
222224 message: cause instanceof Error ? cause.message : fallbackMessage,
......@@ -225,7 +227,7 @@ const makeWsRpcLayer = (currentSessionId: AuthSessionId) =>
225227
226228 const toBootstrapDispatchCommandCauseError = (cause: Cause.Cause<unknown>) => {
227229 const error = Cause.squash(cause);
228 return Schema.is(OrchestrationDispatchCommandError)(error)
230 return isOrchestrationDispatchCommandError(error)
229231 ? error
230232 : new OrchestrationDispatchCommandError({
231233 message:
......@@ -637,7 +639,7 @@ const makeWsRpcLayer = (currentSessionId: AuthSessionId) =>
637639 return result;
638640 }).pipe(
639641 Effect.mapError((cause) =>
640 Schema.is(OrchestrationDispatchCommandError)(cause)
642 isOrchestrationDispatchCommandError(cause)
641643 ? cause
642644 : new OrchestrationDispatchCommandError({
643645 message: "Failed to dispatch orchestration command",
......@@ -918,7 +920,7 @@ const makeWsRpcLayer = (currentSessionId: AuthSessionId) =>
918920 WS_METHODS.projectsWriteFile,
919921 workspaceFileSystem.writeFile(input).pipe(
920922 Effect.mapError((cause) => {
921 const message = Schema.is(WorkspacePathOutsideRootError)(cause)
923 const message = isWorkspacePathOutsideRootError(cause)
922924 ? "Workspace file path must stay within the project root."
923925 : "Failed to write workspace file";
924926 return new ProjectWriteFileError({
apps/web/src/components/settings/ProviderSettingsForm.tsx+4-6
......@@ -1,6 +1,7 @@
11"use client";
22
33import { useMemo, type ReactNode } from "react";
4import * as Option from "effect/Option";
45import * as Schema from "effect/Schema";
56import type {
67 ProviderSettingsFormAnnotation,
......@@ -63,12 +64,9 @@ function readProviderSettingsFormSchemaAnnotation(
6364function readFieldBooleanDefault(
6465 fieldSchema: ProviderClientDefinition["settingsSchema"]["fields"][string],
6566): boolean | undefined {
66 try {
67 const decoded = Schema.decodeUnknownSync(fieldSchema as Schema.Decoder<unknown>)(undefined);
68 return typeof decoded === "boolean" ? decoded : undefined;
69 } catch {
70 return undefined;
71 }
67 const decodeDefault = Schema.decodeUnknownOption(fieldSchema as Schema.Decoder<unknown>);
68 const decoded = decodeDefault(undefined);
69 return Option.isSome(decoded) && typeof decoded.value === "boolean" ? decoded.value : undefined;
7270}
7371
7472export function deriveProviderSettingsFields(
apps/web/src/composerDraftStore.ts+3-2
......@@ -42,11 +42,12 @@ import { useShallow } from "zustand/react/shallow";
4242import { createDebouncedStorage, createMemoryStorage } from "./lib/storage";
4343import { getDefaultServerModel } from "./providerModels";
4444import { UnifiedSettings } from "@t3tools/contracts/settings";
45const isRuntimeMode = Schema.is(RuntimeMode);
46const isProviderDriverKind = Schema.is(ProviderDriverKind);
4547
4648export const COMPOSER_DRAFT_STORAGE_KEY = "t3code:composer-drafts:v1";
4749const COMPOSER_DRAFT_STORAGE_VERSION = 6;
4850const DraftThreadEnvModeSchema = Schema.Literals(["local", "worktree"]);
49const isRuntimeMode = Schema.is(RuntimeMode);
5051export type DraftThreadEnvMode = typeof DraftThreadEnvModeSchema.Type;
5152
5253export const DraftId = Schema.String.pipe(Schema.brand("DraftId"));
......@@ -575,7 +576,7 @@ function shouldRemoveDraft(draft: ComposerThreadDraftState): boolean {
575576}
576577
577578function normalizeProviderDriverKind(value: unknown): ProviderDriverKind | null {
578 return Schema.is(ProviderDriverKind)(value) ? value : null;
579 return isProviderDriverKind(value) ? value : null;
579580}
580581
581582/**
apps/web/src/hooks/useLocalStorage.ts+8-4
......@@ -19,11 +19,15 @@ const isomorphicLocalStorage: Storage =
1919 };
2020 })();
2121
22const decode = <T, E>(schema: Schema.Codec<T, E>, value: string) =>
23 Schema.decodeSync(Schema.fromJsonString(schema))(value);
22const decode = <T, E>(schema: Schema.Codec<T, E>, value: string) => {
23 const decodeJson = Schema.decodeSync(Schema.fromJsonString(schema));
24 return decodeJson(value);
25};
2426
25const encode = <T, E>(schema: Schema.Codec<T, E>, value: T) =>
26 Schema.encodeSync(Schema.fromJsonString(schema))(value);
27const encode = <T, E>(schema: Schema.Codec<T, E>, value: T) => {
28 const encodeJson = Schema.encodeSync(Schema.fromJsonString(schema));
29 return encodeJson(value);
30};
2731
2832export const getLocalStorageItem = <T, E>(key: string, schema: Schema.Codec<T, E>): T | null => {
2933 const item = isomorphicLocalStorage.getItem(key);
apps/web/src/lib/projectScriptKeybindings.ts+3-1
......@@ -8,6 +8,8 @@ import * as Schema from "effect/Schema";
88
99export const PROJECT_SCRIPT_KEYBINDING_INVALID_MESSAGE = "Invalid keybinding.";
1010
11const decodeKeybindingRule = Schema.decodeUnknownOption(KeybindingRuleSchema);
12
1113function normalizeProjectScriptKeybindingInput(
1214 keybinding: string | null | undefined,
1315): string | null {
......@@ -22,7 +24,7 @@ export function decodeProjectScriptKeybindingRule(input: {
2224 const normalizedKey = normalizeProjectScriptKeybindingInput(input.keybinding);
2325 if (!normalizedKey) return null;
2426
25 const decoded = Schema.decodeUnknownOption(KeybindingRuleSchema)({
27 const decoded = decodeKeybindingRule({
2628 key: normalizedKey,
2729 command: input.command,
2830 });
apps/web/src/lib/providerReactQuery.ts+5-2
......@@ -9,6 +9,9 @@ import * as Option from "effect/Option";
99import * as Schema from "effect/Schema";
1010import { ensureEnvironmentApi } from "../environmentApi";
1111
12const decodeFullThreadDiffInput = Schema.decodeUnknownOption(OrchestrationGetFullThreadDiffInput);
13const decodeTurnDiffInput = Schema.decodeUnknownOption(OrchestrationGetTurnDiffInput);
14
1215interface CheckpointDiffQueryInput {
1316 environmentId: EnvironmentId | null;
1417 threadId: ThreadId | null;
......@@ -36,14 +39,14 @@ export const providerQueryKeys = {
3639
3740function decodeCheckpointDiffRequest(input: CheckpointDiffQueryInput) {
3841 if (input.fromTurnCount === 0) {
39 return Schema.decodeUnknownOption(OrchestrationGetFullThreadDiffInput)({
42 return decodeFullThreadDiffInput({
4043 threadId: input.threadId,
4144 toTurnCount: input.toTurnCount,
4245 ignoreWhitespace: input.ignoreWhitespace,
4346 }).pipe(Option.map((fields) => ({ kind: "fullThreadDiff" as const, input: fields })));
4447 }
4548
46 return Schema.decodeUnknownOption(OrchestrationGetTurnDiffInput)({
49 return decodeTurnDiffInput({
4750 threadId: input.threadId,
4851 fromTurnCount: input.fromTurnCount,
4952 toTurnCount: input.toTurnCount,
apps/web/src/projectScripts.ts+2-1
......@@ -5,6 +5,7 @@ import {
55 type ProjectScript,
66} from "@t3tools/contracts";
77import * as Schema from "effect/Schema";
8const isScriptRunCommand = Schema.is(SCRIPT_RUN_COMMAND_PATTERN);
89
910function normalizeScriptId(value: string): string {
1011 const cleaned = value
......@@ -26,7 +27,7 @@ export const commandForProjectScript = (scriptId: string): KeybindingCommand =>
2627
2728export function projectScriptIdFromCommand(command: string): string | null {
2829 const trimmed = command.trim();
29 if (!Schema.is(SCRIPT_RUN_COMMAND_PATTERN)(trimmed)) {
30 if (!isScriptRunCommand(trimmed)) {
3031 return null;
3132 }
3233 const [prefix, , suffix] = SCRIPT_RUN_COMMAND_PATTERN.parts;
apps/web/src/store.ts+2-1
......@@ -37,6 +37,7 @@ import {
3737import { resolveEnvironmentHttpUrl } from "./environments/runtime";
3838import { sanitizeThreadErrorMessage } from "./rpc/transportError";
3939import { getThreadFromEnvironmentState } from "./threadDerivation";
40const isProviderDriverKindValue = Schema.is(ProviderDriverKind);
4041
4142export interface EnvironmentState {
4243 projectIds: ProjectId[];
......@@ -1006,7 +1007,7 @@ function toLegacySessionStatus(
10061007}
10071008
10081009function toLegacyProvider(providerName: string | null): ProviderDriverKind {
1009 if (Schema.is(ProviderDriverKind)(providerName)) {
1010 if (isProviderDriverKindValue(providerName)) {
10101011 return providerName;
10111012 }
10121013 return ProviderDriverKind.make("codex");
apps/web/src/uiStateStore.test.ts+20-20
......@@ -445,27 +445,27 @@ describe("uiStateStore pure functions", () => {
445445 });
446446});
447447
448describe("uiStateStore persistence round-trip", () => {
449 function createLocalStorageStub(): Storage {
450 const store = new Map<string, string>();
451 return {
452 clear: () => {
453 store.clear();
454 },
455 getItem: (key) => store.get(key) ?? null,
456 key: (index) => [...store.keys()][index] ?? null,
457 get length() {
458 return store.size;
459 },
460 removeItem: (key) => {
461 store.delete(key);
462 },
463 setItem: (key, value) => {
464 store.set(key, value);
465 },
466 };
467 }
448function createLocalStorageStub(): Storage {
449 const store = new Map<string, string>();
450 return {
451 clear: () => {
452 store.clear();
453 },
454 getItem: (key) => store.get(key) ?? null,
455 key: (index) => [...store.keys()][index] ?? null,
456 get length() {
457 return store.size;
458 },
459 removeItem: (key) => {
460 store.delete(key);
461 },
462 setItem: (key, value) => {
463 store.set(key, value);
464 },
465 };
466}
468467
468describe("uiStateStore persistence round-trip", () => {
469469 let localStorageStub: Storage;
470470
471471 beforeEach(() => {
bun.lock+41-21
......@@ -6,9 +6,10 @@
66 "name": "@t3tools/monorepo",
77 "devDependencies": {
88 "@effect/language-service": "catalog:",
9 "@oxlint/plugins": "^1.63.0",
910 "@types/node": "catalog:",
1011 "oxfmt": "^0.40.0",
11 "oxlint": "^1.55.0",
12 "oxlint": "^1.63.0",
1213 "turbo": "^2.3.3",
1314 "typescript": "catalog:",
1415 "vitest": "catalog:",
......@@ -135,6 +136,21 @@
135136 "vitest-browser-react": "^2.0.5",
136137 },
137138 },
139 "oxlint-plugin-t3code": {
140 "name": "@t3tools/oxlint-plugin-t3code",
141 "dependencies": {
142 "@effect/platform-node": "catalog:",
143 "@oxlint/plugins": "^1.63.0",
144 "effect": "catalog:",
145 },
146 "devDependencies": {
147 "@effect/language-service": "catalog:",
148 "@effect/vitest": "catalog:",
149 "@types/bun": "catalog:",
150 "typescript": "catalog:",
151 "vitest": "catalog:",
152 },
153 },
138154 "packages/client-runtime": {
139155 "name": "@t3tools/client-runtime",
140156 "version": "0.0.0-alpha.1",
......@@ -667,43 +683,45 @@
667683
668684 "@oxfmt/binding-win32-x64-msvc": ["@oxfmt/binding-win32-x64-msvc@0.40.0", "", { "os": "win32", "cpu": "x64" }, "sha512-/Zmj0yTYSvmha6TG1QnoLqVT7ZMRDqXvFXXBQpIjteEwx9qvUYMBH2xbiOFhDeMUJkGwC3D6fdKsFtaqUvkwNA=="],
669685
670 "@oxlint/binding-android-arm-eabi": ["@oxlint/binding-android-arm-eabi@1.56.0", "", { "os": "android", "cpu": "arm" }, "sha512-IyfYPthZyiSKwAv/dLjeO18SaK8MxLI9Yss2JrRDyweQAkuL3LhEy7pwIwI7uA3KQc1Vdn20kdmj3q0oUIQL6A=="],
686 "@oxlint/binding-android-arm-eabi": ["@oxlint/binding-android-arm-eabi@1.63.0", "", { "os": "android", "cpu": "arm" }, "sha512-A9xLtQt7i0OA1PoB/meog6kikXI9CdwEp7ZwQqmgnpKn3G3b1orvTDy8CQ6T7w1HvDrgWGB78PkFKcWgibcTCg=="],
687
688 "@oxlint/binding-android-arm64": ["@oxlint/binding-android-arm64@1.63.0", "", { "os": "android", "cpu": "arm64" }, "sha512-SQo+ZMvdR9l3CxZp5W5gFNxSiDxclY6lOzzNpKYLF8asESpm3Pwumx0gER5T7aHLF1/2BAAtLD3DiDkdgy4V1A=="],
671689
672 "@oxlint/binding-android-arm64": ["@oxlint/binding-android-arm64@1.56.0", "", { "os": "android", "cpu": "arm64" }, "sha512-Ga5zYrzH6vc/VFxhn6MmyUnYEfy9vRpwTIks99mY3j6Nz30yYpIkWryI0QKPCgvGUtDSXVLEaMum5nA+WrNOSg=="],
690 "@oxlint/binding-darwin-arm64": ["@oxlint/binding-darwin-arm64@1.63.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-6W82XjJDTmMnjg30427l0dufpnyLoq7wEukKdM6/g2VIybRVuQiBVh43EA4b+UxZ3+tLcKm+Or/pXGNgLCEU8g=="],
673691
674 "@oxlint/binding-darwin-arm64": ["@oxlint/binding-darwin-arm64@1.56.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-ogmbdJysnw/D4bDcpf1sPLpFThZ48lYp4aKYm10Z/6Nh1SON6NtnNhTNOlhEY296tDFItsZUz+2tgcSYqh8Eyw=="],
692 "@oxlint/binding-darwin-x64": ["@oxlint/binding-darwin-x64@1.63.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-CnWd/YCuVG5W1BYkjJEVbJG11o526O9qAwBEQM+nh8K19CRFUkFdROXCyYkGmroHEYQe4vgQ6+lh3550Lp35Xw=="],
675693
676 "@oxlint/binding-darwin-x64": ["@oxlint/binding-darwin-x64@1.56.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-x8QE1h+RAtQ2g+3KPsP6Fk/tdz6zJQUv5c7fTrJxXV3GHOo+Ry5p/PsogU4U+iUZg0rj6hS+E4xi+mnwwlDCWQ=="],
694 "@oxlint/binding-freebsd-x64": ["@oxlint/binding-freebsd-x64@1.63.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-a4eZAqrmtajqcxfdAzC+l7g3PaE3V8hpAYqqeD3fTxLXOMFdK3eNTZrU80n4dDEVm0JXy1aL5PqvqWldBl6zYA=="],
677695
678 "@oxlint/binding-freebsd-x64": ["@oxlint/binding-freebsd-x64@1.56.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-6G+WMZvwJpMvY7my+/SHEjb7BTk/PFbePqLpmVmUJRIsJMy/UlyYqjpuh0RCgYYkPLcnXm1rUM04kbTk8yS1Yg=="],
696 "@oxlint/binding-linux-arm-gnueabihf": ["@oxlint/binding-linux-arm-gnueabihf@1.63.0", "", { "os": "linux", "cpu": "arm" }, "sha512-tYUtU9TdbU3uXF5D62g5zXJ13iniFGhXQx5vp9cyEjGdbSAY3VdFBSaldYvyoDmgMZ0ZYuwQP1Y4t2Fhejwa0w=="],
679697
680 "@oxlint/binding-linux-arm-gnueabihf": ["@oxlint/binding-linux-arm-gnueabihf@1.56.0", "", { "os": "linux", "cpu": "arm" }, "sha512-YYHBsk/sl7fYwQOok+6W5lBPeUEvisznV/HZD2IfZmF3Bns6cPC3Z0vCtSEOaAWTjYWN3jVsdu55jMxKlsdlhg=="],
698 "@oxlint/binding-linux-arm-musleabihf": ["@oxlint/binding-linux-arm-musleabihf@1.63.0", "", { "os": "linux", "cpu": "arm" }, "sha512-I5r3twFf776UZg9dmRo2xbrKt00tTkORXEVe0ctg4vdTkQvJAjiCHxnbAU2HL1AiJ9cqADA76MAliuilsAWnvg=="],
681699
682 "@oxlint/binding-linux-arm-musleabihf": ["@oxlint/binding-linux-arm-musleabihf@1.56.0", "", { "os": "linux", "cpu": "arm" }, "sha512-+AZK8rOUr78y8WT6XkDb04IbMRqauNV+vgT6f8ZLOH8wnpQ9i7Nol0XLxAu+Cq7Sb+J9wC0j6Km5hG8rj47/yQ=="],
700 "@oxlint/binding-linux-arm64-gnu": ["@oxlint/binding-linux-arm64-gnu@1.63.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-t7ltUkg6FFh4b564QyGir8xIj/QZbXu8FlcRkcyW9+ztr/mfRHlvUOFd95pJCXi9s/L5DrUeWWgpXRS+V+6igQ=="],
683701
684 "@oxlint/binding-linux-arm64-gnu": ["@oxlint/binding-linux-arm64-gnu@1.56.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-urse2SnugwJRojUkGSSeH2LPMaje5Q50yQtvtL9HFckiyeqXzoFwOAZqD5TR29R2lq7UHidfFDM9EGcchcbb8A=="],
702 "@oxlint/binding-linux-arm64-musl": ["@oxlint/binding-linux-arm64-musl@1.63.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-Q5mmZy/XWjuYFUuQyYjOvZ5U/JkKEwnpir6hGxhh6HcdP0V/BKxLo8dqkfF/t7r7AguB17dfS/8+go5AQDRR6g=="],
685703
686 "@oxlint/binding-linux-arm64-musl": ["@oxlint/binding-linux-arm64-musl@1.56.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-rkTZkBfJ4TYLjansjSzL6mgZOdN5IvUnSq3oNJSLwBcNvy3dlgQtpHPrRxrCEbbcp7oQ6If0tkNaqfOsphYZ9g=="],
704 "@oxlint/binding-linux-ppc64-gnu": ["@oxlint/binding-linux-ppc64-gnu@1.63.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-uBGtuZ0TzLB4x5wVa82HGNvYqY8buwDhyCnCP0R0gkk9szqVsP0MeTtD5HX7EsEuFIt+aYmYxuxeVxs3nTSwtQ=="],
687705
688 "@oxlint/binding-linux-ppc64-gnu": ["@oxlint/binding-linux-ppc64-gnu@1.56.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-uqL1kMH3u69/e1CH2EJhP3CP28jw2ExLsku4o8RVAZ7fySo9zOyI2fy9pVlTAp4voBLVgzndXi3SgtdyCTa2aA=="],
706 "@oxlint/binding-linux-riscv64-gnu": ["@oxlint/binding-linux-riscv64-gnu@1.63.0", "", { "os": "linux", "cpu": "none" }, "sha512-h4s6FwxE+9MeA181o0dnDwHP32Y/bG8EiB/vrD6Ib+AMt6haigDc/0bUtI/sLmQDBMJnUfaCmtSSrEAqjtEVrA=="],
689707
690 "@oxlint/binding-linux-riscv64-gnu": ["@oxlint/binding-linux-riscv64-gnu@1.56.0", "", { "os": "linux", "cpu": "none" }, "sha512-j0CcMBOgV6KsRaBdsebIeiy7hCjEvq2KdEsiULf2LZqAq0v1M1lWjelhCV57LxsqaIGChXFuFJ0RiFrSRHPhSg=="],
708 "@oxlint/binding-linux-riscv64-musl": ["@oxlint/binding-linux-riscv64-musl@1.63.0", "", { "os": "linux", "cpu": "none" }, "sha512-2EaNcCBR8Mcjl5ARtuN3BdEpVkX7KpjSjMGZ/mJMIeaXgTtdz5ytg2VwygMSStA/k0ixfvZFoZOfjDEcouV5vQ=="],
691709
692 "@oxlint/binding-linux-riscv64-musl": ["@oxlint/binding-linux-riscv64-musl@1.56.0", "", { "os": "linux", "cpu": "none" }, "sha512-7VDOiL8cDG3DQ/CY3yKjbV1c4YPvc4vH8qW09Vv+5ukq3l/Kcyr6XGCd5NvxUmxqDb2vjMpM+eW/4JrEEsUetA=="],
710 "@oxlint/binding-linux-s390x-gnu": ["@oxlint/binding-linux-s390x-gnu@1.63.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-p4hlf/fd7TrYYl3QrWWD0GocqJefwMu3cHQhmi2FvEB/YOvFb5DZN3SMBaPi7B1TM5DeypkEtrVib674q1KKPg=="],
693711
694 "@oxlint/binding-linux-s390x-gnu": ["@oxlint/binding-linux-s390x-gnu@1.56.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-JGRpX0M+ikD3WpwJ7vKcHKV6Kg0dT52BW2Eu2BupXotYeqGXBrbY+QPkAyKO6MNgKozyTNaRh3r7g+VWgyAQYQ=="],
712 "@oxlint/binding-linux-x64-gnu": ["@oxlint/binding-linux-x64-gnu@1.63.0", "", { "os": "linux", "cpu": "x64" }, "sha512-Vgq9rkRVcPcjbcH+ihYTfpeR7vCXfqpd+z5ItTGc0yYUV59L5ceHYN1iV4H9bKGV7Rn5hkVc7x3mSvHegduENA=="],
695713
696 "@oxlint/binding-linux-x64-gnu": ["@oxlint/binding-linux-x64-gnu@1.56.0", "", { "os": "linux", "cpu": "x64" }, "sha512-dNaICPvtmuxFP/VbqdofrLqdS3bM/AKJN3LMJD52si44ea7Be1cBk6NpfIahaysG9Uo+L98QKddU9CD5L8UHnQ=="],
714 "@oxlint/binding-linux-x64-musl": ["@oxlint/binding-linux-x64-musl@1.63.0", "", { "os": "linux", "cpu": "x64" }, "sha512-3/Lkq/ncooA61rorrC+ZQed1Bc4VpGj+WnGsp58zmxKgvZ2vhreu+dcVyr3mX8NUpq7mfZ4gDDTou/yrF1Pd7A=="],
697715
698 "@oxlint/binding-linux-x64-musl": ["@oxlint/binding-linux-x64-musl@1.56.0", "", { "os": "linux", "cpu": "x64" }, "sha512-pF1vOtM+GuXmbklM1hV8WMsn6tCNPvkUzklj/Ej98JhlanbmA2RB1BILgOpwSuCTRTIYx2MXssmEyQQ90QF5aA=="],
716 "@oxlint/binding-openharmony-arm64": ["@oxlint/binding-openharmony-arm64@1.63.0", "", { "os": "none", "cpu": "arm64" }, "sha512-0/EdD/6hDkx5Mfd769PTjvEM8mZ/6Dfukp1dBCL/2PjlIVGEtYdNZyok6ChqYPsT9JcFnlQnUeQzO0/1L/oC9w=="],
699717
700 "@oxlint/binding-openharmony-arm64": ["@oxlint/binding-openharmony-arm64@1.56.0", "", { "os": "none", "cpu": "arm64" }, "sha512-bp8NQ4RE6fDIFLa4bdBiOA+TAvkNkg+rslR+AvvjlLTYXLy9/uKAYLQudaQouWihLD/hgkrXIKKzXi5IXOewwg=="],
718 "@oxlint/binding-win32-arm64-msvc": ["@oxlint/binding-win32-arm64-msvc@1.63.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-wb0CUkN8ngwPiRQBjD1Cj0LsHeNvm+Xt6YBHDMtj2DVQVD6Oj8Ri7g6BD+KICf6LaBqZlmzOvy6nF9E/8yyGOg=="],
701719
702 "@oxlint/binding-win32-arm64-msvc": ["@oxlint/binding-win32-arm64-msvc@1.56.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-PxT4OJDfMOQBzo3OlzFb9gkoSD+n8qSBxyVq2wQSZIHFQYGEqIRTo9M0ZStvZm5fdhMqaVYpOnJvH2hUMEDk/g=="],
720 "@oxlint/binding-win32-ia32-msvc": ["@oxlint/binding-win32-ia32-msvc@1.63.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-BX5iq+ovdNlVYhSn5qPMUIT0uwAwt2lmEnCnzK+Gkhw4DovIvhGb96OFhV8yzQNUnQxn/xGkOR+X+BLrLDNm8w=="],
703721
704 "@oxlint/binding-win32-ia32-msvc": ["@oxlint/binding-win32-ia32-msvc@1.56.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-PTRy6sIEPqy2x8PTP1baBNReN/BNEFmde0L+mYeHmjXE1Vlcc9+I5nsqENsB2yAm5wLkzPoTNCMY/7AnabT4/A=="],
722 "@oxlint/binding-win32-x64-msvc": ["@oxlint/binding-win32-x64-msvc@1.63.0", "", { "os": "win32", "cpu": "x64" }, "sha512-QeN/WELOfsXMeYwxvfgQrl6CbVftYUCZsGXHjXQd5Trccm8+i4gmtxaOui4xbJQaiDlviF8F3yLSBloQUeFsfA=="],
705723
706 "@oxlint/binding-win32-x64-msvc": ["@oxlint/binding-win32-x64-msvc@1.56.0", "", { "os": "win32", "cpu": "x64" }, "sha512-ZHa0clocjLmIDr+1LwoWtxRcoYniAvERotvwKUYKhH41NVfl0Y4LNbyQkwMZzwDvKklKGvGZ5+DAG58/Ik47tQ=="],
724 "@oxlint/plugins": ["@oxlint/plugins@1.63.0", "", {}, "sha512-vZAzaUQkwgdN62RHgPFzfCsiBI6SDJMUdUlBGpJK0V++UHCMUk7UeJseygOhE/wOUSgV3ccE4ORkgab3C3MC6g=="],
707725
708726 "@pierre/diffs": ["@pierre/diffs@1.1.20", "", { "dependencies": { "@pierre/theme": "0.0.28", "@shikijs/transformers": "^3.0.0", "diff": "8.0.3", "hast-util-to-html": "9.0.5", "lru_map": "0.4.1", "shiki": "^3.0.0" }, "peerDependencies": { "react": "^18.3.1 || ^19.0.0", "react-dom": "^18.3.1 || ^19.0.0" } }, "sha512-lLi+3sLCm3QDd5/aLO9pw+WbF6UzhrkWm2oTZ5WZJTGemOyUNRJ4DDhcEKmVusu4C4bXx9Nssh6fF+wQcapb5w=="],
709727
......@@ -783,6 +801,8 @@
783801
784802 "@t3tools/marketing": ["@t3tools/marketing@workspace:apps/marketing"],
785803
804 "@t3tools/oxlint-plugin-t3code": ["@t3tools/oxlint-plugin-t3code@workspace:oxlint-plugin-t3code"],
805
786806 "@t3tools/scripts": ["@t3tools/scripts@workspace:scripts"],
787807
788808 "@t3tools/shared": ["@t3tools/shared@workspace:packages/shared"],
......@@ -1639,7 +1659,7 @@
16391659
16401660 "oxfmt": ["oxfmt@0.40.0", "", { "dependencies": { "tinypool": "2.1.0" }, "optionalDependencies": { "@oxfmt/binding-android-arm-eabi": "0.40.0", "@oxfmt/binding-android-arm64": "0.40.0", "@oxfmt/binding-darwin-arm64": "0.40.0", "@oxfmt/binding-darwin-x64": "0.40.0", "@oxfmt/binding-freebsd-x64": "0.40.0", "@oxfmt/binding-linux-arm-gnueabihf": "0.40.0", "@oxfmt/binding-linux-arm-musleabihf": "0.40.0", "@oxfmt/binding-linux-arm64-gnu": "0.40.0", "@oxfmt/binding-linux-arm64-musl": "0.40.0", "@oxfmt/binding-linux-ppc64-gnu": "0.40.0", "@oxfmt/binding-linux-riscv64-gnu": "0.40.0", "@oxfmt/binding-linux-riscv64-musl": "0.40.0", "@oxfmt/binding-linux-s390x-gnu": "0.40.0", "@oxfmt/binding-linux-x64-gnu": "0.40.0", "@oxfmt/binding-linux-x64-musl": "0.40.0", "@oxfmt/binding-openharmony-arm64": "0.40.0", "@oxfmt/binding-win32-arm64-msvc": "0.40.0", "@oxfmt/binding-win32-ia32-msvc": "0.40.0", "@oxfmt/binding-win32-x64-msvc": "0.40.0" }, "bin": { "oxfmt": "bin/oxfmt" } }, "sha512-g0C3I7xUj4b4DcagevM9kgH6+pUHytikxUcn3/VUkvzTNaaXBeyZqb7IBsHwojeXm4mTBEC/aBjBTMVUkZwWUQ=="],
16411661
1642 "oxlint": ["oxlint@1.56.0", "", { "optionalDependencies": { "@oxlint/binding-android-arm-eabi": "1.56.0", "@oxlint/binding-android-arm64": "1.56.0", "@oxlint/binding-darwin-arm64": "1.56.0", "@oxlint/binding-darwin-x64": "1.56.0", "@oxlint/binding-freebsd-x64": "1.56.0", "@oxlint/binding-linux-arm-gnueabihf": "1.56.0", "@oxlint/binding-linux-arm-musleabihf": "1.56.0", "@oxlint/binding-linux-arm64-gnu": "1.56.0", "@oxlint/binding-linux-arm64-musl": "1.56.0", "@oxlint/binding-linux-ppc64-gnu": "1.56.0", "@oxlint/binding-linux-riscv64-gnu": "1.56.0", "@oxlint/binding-linux-riscv64-musl": "1.56.0", "@oxlint/binding-linux-s390x-gnu": "1.56.0", "@oxlint/binding-linux-x64-gnu": "1.56.0", "@oxlint/binding-linux-x64-musl": "1.56.0", "@oxlint/binding-openharmony-arm64": "1.56.0", "@oxlint/binding-win32-arm64-msvc": "1.56.0", "@oxlint/binding-win32-ia32-msvc": "1.56.0", "@oxlint/binding-win32-x64-msvc": "1.56.0" }, "peerDependencies": { "oxlint-tsgolint": ">=0.15.0" }, "optionalPeers": ["oxlint-tsgolint"], "bin": { "oxlint": "bin/oxlint" } }, "sha512-Q+5Mj5PVaH/R6/fhMMFzw4dT+KPB+kQW4kaL8FOIq7tfhlnEVp6+3lcWqFruuTNlUo9srZUW3qH7Id4pskeR6g=="],
1662 "oxlint": ["oxlint@1.63.0", "", { "optionalDependencies": { "@oxlint/binding-android-arm-eabi": "1.63.0", "@oxlint/binding-android-arm64": "1.63.0", "@oxlint/binding-darwin-arm64": "1.63.0", "@oxlint/binding-darwin-x64": "1.63.0", "@oxlint/binding-freebsd-x64": "1.63.0", "@oxlint/binding-linux-arm-gnueabihf": "1.63.0", "@oxlint/binding-linux-arm-musleabihf": "1.63.0", "@oxlint/binding-linux-arm64-gnu": "1.63.0", "@oxlint/binding-linux-arm64-musl": "1.63.0", "@oxlint/binding-linux-ppc64-gnu": "1.63.0", "@oxlint/binding-linux-riscv64-gnu": "1.63.0", "@oxlint/binding-linux-riscv64-musl": "1.63.0", "@oxlint/binding-linux-s390x-gnu": "1.63.0", "@oxlint/binding-linux-x64-gnu": "1.63.0", "@oxlint/binding-linux-x64-musl": "1.63.0", "@oxlint/binding-openharmony-arm64": "1.63.0", "@oxlint/binding-win32-arm64-msvc": "1.63.0", "@oxlint/binding-win32-ia32-msvc": "1.63.0", "@oxlint/binding-win32-x64-msvc": "1.63.0" }, "peerDependencies": { "oxlint-tsgolint": ">=0.22.1" }, "optionalPeers": ["oxlint-tsgolint"], "bin": { "oxlint": "bin/oxlint" } }, "sha512-9TGXetdjgIHOJ9OiReomP7nnrMkV9HxC1xM2ramJSLQpzxjsAJtQwa4wqkJN2f/uCrqZuJseFuSlWDdvcruveg=="],
16431663
16441664 "p-cancelable": ["p-cancelable@2.1.1", "", {}, "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg=="],
16451665
oxlint-plugin-t3code/index.ts created+12
......@@ -0,0 +1,12 @@
1import { definePlugin } from "@oxlint/plugins";
2
3import noInlineSchemaCompile from "./rules/no-inline-schema-compile.ts";
4
5export default definePlugin({
6 meta: {
7 name: "t3code",
8 },
9 rules: {
10 "no-inline-schema-compile": noInlineSchemaCompile,
11 },
12});
oxlint-plugin-t3code/package.json created+21
......@@ -0,0 +1,21 @@
1{
2 "name": "@t3tools/oxlint-plugin-t3code",
3 "private": true,
4 "type": "module",
5 "scripts": {
6 "typecheck": "tsc --noEmit",
7 "test": "vitest run"
8 },
9 "dependencies": {
10 "@effect/platform-node": "catalog:",
11 "@oxlint/plugins": "^1.63.0",
12 "effect": "catalog:"
13 },
14 "devDependencies": {
15 "@effect/language-service": "catalog:",
16 "@effect/vitest": "catalog:",
17 "@types/bun": "catalog:",
18 "typescript": "catalog:",
19 "vitest": "catalog:"
20 }
21}
oxlint-plugin-t3code/rules/no-inline-schema-compile.test.ts created+83
......@@ -0,0 +1,83 @@
1import { assert, describe } from "@effect/vitest";
2
3import { createOxlintRuleHarness } from "../test/utils.ts";
4
5const rule = createOxlintRuleHarness("t3code/no-inline-schema-compile");
6
7describe("t3code/no-inline-schema-compile", () => {
8 rule.valid(
9 "allows schema compilers hoisted to module scope",
10 `
11 import { Schema } from "effect";
12
13 const User = Schema.Struct({ name: Schema.String });
14 const decodeUser = Schema.decodeUnknownEffect(User);
15
16 export const parseUser = (input: unknown) => decodeUser(input);
17 `,
18 );
19
20 rule.valid(
21 "allows factory helpers that return a precompiled decoder",
22 `
23 import { Schema } from "effect";
24
25 export const makeParser = <A, I>(schema: Schema.Codec<A, I>) => {
26 const decode = Schema.decodeUnknownEffect(schema);
27 return (input: unknown) => decode(input);
28 };
29 `,
30 );
31
32 rule.valid(
33 "allows schema construction helpers that use encode transformations",
34 `
35 import { Schema } from "effect";
36
37 export const makePrettyJson = <S extends Schema.Top>(schema: S) =>
38 Schema.fromJsonString(schema).pipe(
39 Schema.encode({
40 decode: Schema.String,
41 encode: Schema.String,
42 }),
43 );
44 `,
45 );
46
47 rule.valid(
48 "allows dynamic schema parameters that cannot be hoisted to module scope",
49 `
50 import { Schema } from "effect";
51
52 export const parseWith = <A, I>(schema: Schema.Codec<A, I>, input: unknown) =>
53 Schema.decodeUnknownEffect(schema)(input);
54 `,
55 );
56
57 rule.invalid(
58 "reports schema compilers inside function bodies",
59 `
60 import { Schema } from "effect";
61
62 const User = Schema.Struct({ name: Schema.String });
63
64 export const parseUser = (input: unknown) => Schema.decodeUnknownEffect(User)(input);
65 `,
66 (output) => {
67 assert.match(output, /Hoist Schema\.decodeUnknownEffect/);
68 },
69 );
70
71 rule.invalid(
72 "reports inline schema literals as high confidence findings",
73 `
74 import { Schema } from "effect";
75
76 export const parseUser = (input: unknown) =>
77 Schema.decodeUnknownEffect(Schema.Struct({ name: Schema.String }))(input);
78 `,
79 (output) => {
80 assert.match(output, /inline schema literal and the compiled function/);
81 },
82 );
83});
oxlint-plugin-t3code/rules/no-inline-schema-compile.ts created+150
......@@ -0,0 +1,150 @@
1import { defineRule } from "@oxlint/plugins";
2import * as Option from "effect/Option";
3import * as Schema from "effect/Schema";
4
5import { getPropertyName, isIdentifier, unwrapExpression } from "../utils.ts";
6
7// Effect Schema decoder/encoder APIs allocate compiled functions. Keep them
8// outside function bodies so hot paths do not rebuild compilers per call.
9const COMPILER_METHODS = new Set<keyof typeof Schema>([
10 "is",
11 "asserts",
12 "decodeEffect",
13 "decodeExit",
14 "decodeOption",
15 "decodePromise",
16 "decodeSync",
17 "decodeUnknownExit",
18 "decodeUnknownEffect",
19 "decodeUnknownOption",
20 "decodeUnknownPromise",
21 "decodeUnknownSync",
22
23 "encodeExit",
24 "encodeEffect",
25 "encodeOption",
26 "encodePromise",
27 "encodeSync",
28 "encodeUnknownExit",
29 "encodeUnknownEffect",
30 "encodeUnknownOption",
31 "encodeUnknownPromise",
32 "encodeUnknownSync",
33]);
34
35const getSchemaCompilerMethod = (callee: unknown): Option.Option<string> => {
36 const expression = unwrapExpression(callee);
37 if (Option.isNone(expression) || expression.value.type !== "MemberExpression") {
38 return Option.none();
39 }
40
41 const object = unwrapExpression(expression.value.object);
42 if (!isIdentifier(object, "Schema")) return Option.none();
43
44 return Option.filter(getPropertyName(expression.value.property), (method) =>
45 COMPILER_METHODS.has(method as keyof typeof Schema),
46 );
47};
48
49const isStaticSchemaReference = (node: unknown): boolean => {
50 const expression = unwrapExpression(node);
51 if (Option.isNone(expression)) return false;
52
53 if (expression.value.type === "Identifier") {
54 const [firstChar] = expression.value.name;
55 return firstChar !== undefined && firstChar.toUpperCase() === firstChar;
56 }
57
58 return expression.value.type === "MemberExpression";
59};
60
61const isNestedStaticSchemaCall = (node: unknown): boolean => {
62 const expression = unwrapExpression(node);
63 if (Option.isNone(expression) || expression.value.type !== "CallExpression") return false;
64
65 const callee = unwrapExpression(expression.value.callee);
66 if (Option.isNone(callee) || callee.value.type !== "MemberExpression") return false;
67
68 const object = unwrapExpression(callee.value.object);
69 if (!isIdentifier(object, "Schema")) return false;
70
71 const method = getPropertyName(callee.value.property);
72 if (Option.isSome(method) && method.value === "fromJsonString") {
73 const firstArg = expression.value.arguments[0];
74 return isStaticSchemaReference(firstArg) || isNestedStaticSchemaCall(firstArg);
75 }
76
77 return true;
78};
79
80const isImmediatelyInvoked = (node: unknown): boolean => {
81 const expression = unwrapExpression(node);
82 if (Option.isNone(expression)) return false;
83
84 const parent =
85 "parent" in expression.value ? unwrapExpression(expression.value.parent) : Option.none();
86 return (
87 Option.isSome(parent) &&
88 parent.value.type === "CallExpression" &&
89 unwrapExpression(parent.value.callee).pipe(
90 Option.exists((callee) => callee === expression.value),
91 )
92 );
93};
94
95const messageHigh = (method: string) =>
96 `Hoist Schema.${method}(...) to module scope: both the inline schema literal and the compiled function are rebuilt on every call. Move the compiled function to a module-level const.`;
97
98const messageMedium = (method: string) =>
99 `Hoist Schema.${method}(...) to module scope: the compiled function is rebuilt on every call. Move it to a module-level const.`;
100
101export default defineRule({
102 meta: {
103 type: "problem",
104 docs: {
105 description:
106 "Disallow Schema decoder/encoder compiler calls inside function bodies; hoist them to module scope.",
107 },
108 },
109 createOnce(context) {
110 let functionDepth = 0;
111
112 const resetFunctionDepth = () => {
113 functionDepth = 0;
114 };
115
116 const enterFunction = () => {
117 functionDepth++;
118 };
119
120 const exitFunction = () => {
121 functionDepth--;
122 };
123
124 return {
125 before: resetFunctionDepth,
126 FunctionDeclaration: enterFunction,
127 "FunctionDeclaration:exit": exitFunction,
128 FunctionExpression: enterFunction,
129 "FunctionExpression:exit": exitFunction,
130 ArrowFunctionExpression: enterFunction,
131 "ArrowFunctionExpression:exit": exitFunction,
132 CallExpression(node) {
133 if (functionDepth === 0) return;
134
135 const method = getSchemaCompilerMethod(node.callee);
136 if (Option.isNone(method)) return;
137 if (!isImmediatelyInvoked(node)) return;
138
139 const firstArg = node.arguments[0];
140 const high = firstArg && isNestedStaticSchemaCall(firstArg);
141 if (!high && !isStaticSchemaReference(firstArg)) return;
142
143 context.report({
144 node: node.callee,
145 message: high ? messageHigh(method.value) : messageMedium(method.value),
146 });
147 },
148 };
149 },
150});
oxlint-plugin-t3code/test/utils.ts created+152
......@@ -0,0 +1,152 @@
1import * as NodeServices from "@effect/platform-node/NodeServices";
2import { assert, it } from "@effect/vitest";
3import * as Data from "effect/Data";
4import * as Effect from "effect/Effect";
5import * as FileSystem from "effect/FileSystem";
6import * as Path from "effect/Path";
7import * as PlatformError from "effect/PlatformError";
8import * as Predicate from "effect/Predicate";
9import * as Schema from "effect/Schema";
10import * as Stream from "effect/Stream";
11import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";
12
13class OxlintFixtureFailure extends Data.TaggedError("OxlintFixtureFailure")<{
14 readonly exitCode: number;
15 readonly stdout: string;
16 readonly stderr: string;
17}> {
18 static readonly is = (u: unknown): u is OxlintFixtureFailure =>
19 Predicate.isTagged(u, "OxlintFixtureFailure");
20}
21
22class OxlintFixtureExpectedFailure extends Data.TaggedError("OxlintFixtureExpectedFailure")<{
23 readonly ruleName: string;
24}> {
25 override get message() {
26 return `Expected oxlint to report a failure for rule ${this.ruleName}, but it passed.`;
27 }
28}
29
30const encodeOxlintConfig = Schema.encodeEffect(Schema.UnknownFromJsonString);
31
32interface RuleHarness {
33 readonly run: (
34 source: string,
35 ) => Effect.Effect<
36 string,
37 OxlintFixtureFailure | PlatformError.PlatformError | Schema.SchemaError,
38 NodeServices.NodeServices
39 >;
40 readonly runAndExpectFailure: (
41 source: string,
42 ) => Effect.Effect<
43 string,
44 OxlintFixtureExpectedFailure | PlatformError.PlatformError | Schema.SchemaError,
45 NodeServices.NodeServices
46 >;
47 readonly valid: (name: string, source: string) => void;
48 readonly invalid: (name: string, source: string, assertion?: (output: string) => void) => void;
49}
50
51const collectStreamAsString = <E>(stream: Stream.Stream<Uint8Array, E>): Effect.Effect<string, E> =>
52 stream.pipe(
53 Stream.decodeText(),
54 Stream.runFold(
55 () => "",
56 (acc, chunk) => acc + chunk,
57 ),
58 );
59
60const spawnAndCollectOutput = Effect.fnUntraced(function* (command: ChildProcess.Command) {
61 const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
62 const child = yield* spawner.spawn(command);
63
64 const [stdout, stderr, exitCode] = yield* Effect.all(
65 [
66 collectStreamAsString(child.stdout),
67 collectStreamAsString(child.stderr),
68 child.exitCode.pipe(Effect.map(Number)),
69 ],
70 { concurrency: "unbounded" },
71 );
72
73 return { exitCode, stdout, stderr };
74}, Effect.scoped);
75
76export const createOxlintRuleHarness = (ruleName: string): RuleHarness => {
77 const [pluginName, shortRuleName] = ruleName.split("/");
78 const diagnosticRuleName =
79 pluginName && shortRuleName ? `${pluginName}\\(${shortRuleName}\\)` : ruleName;
80 const test = it.layer(NodeServices.layer);
81
82 const run: RuleHarness["run"] = Effect.fnUntraced(function* (source: string) {
83 const fs = yield* FileSystem.FileSystem;
84 const path = yield* Path.Path;
85 const fixtureDir = yield* fs.makeTempDirectoryScoped({ prefix: "t3code-oxlint-" });
86 const configPath = path.join(fixtureDir, ".oxlintrc.json");
87 const sourcePath = path.join(fixtureDir, "fixture.ts");
88 const repoRoot = path.join(import.meta.dirname, "..", "..");
89 const oxlintBin = path.join(repoRoot, "node_modules", ".bin", "oxlint");
90 const pluginPath = path.join(repoRoot, "oxlint-plugin-t3code", "index.ts");
91
92 yield* fs.writeFileString(
93 configPath,
94 yield* encodeOxlintConfig({
95 jsPlugins: [{ name: "t3code", specifier: pluginPath }],
96 rules: { [ruleName]: "error" },
97 }),
98 );
99 yield* fs.writeFileString(sourcePath, source);
100
101 const output = yield* spawnAndCollectOutput(
102 ChildProcess.make(oxlintBin, ["--config", configPath, sourcePath], { cwd: repoRoot }),
103 );
104
105 if (output.exitCode !== 0) {
106 return yield* new OxlintFixtureFailure({
107 exitCode: output.exitCode,
108 stdout: output.stdout,
109 stderr: output.stderr,
110 });
111 }
112
113 return `${output.stdout}${output.stderr}`;
114 }, Effect.scoped);
115
116 const runAndExpectFailure: RuleHarness["runAndExpectFailure"] = (source) =>
117 run(source).pipe(
118 Effect.matchEffect({
119 onFailure: (error) =>
120 OxlintFixtureFailure.is(error)
121 ? Effect.succeed(
122 `oxlint fixture failed with exit code ${error.exitCode}\n${error.stdout}\n${error.stderr}`,
123 )
124 : Effect.fail(error),
125 onSuccess: () => Effect.fail(new OxlintFixtureExpectedFailure({ ruleName })),
126 }),
127 );
128
129 return {
130 run,
131 runAndExpectFailure,
132 valid(name, source) {
133 test(name, (it) => {
134 it.effect("passes", () => run(source));
135 });
136 },
137 invalid(name, source, assertion) {
138 test(name, (it) => {
139 it.effect("reports the rule diagnostic", () =>
140 runAndExpectFailure(source).pipe(
141 Effect.tap((output) =>
142 Effect.sync(() => {
143 assert.match(output, new RegExp(diagnosticRuleName));
144 assertion?.(output);
145 }),
146 ),
147 ),
148 );
149 });
150 },
151 };
152};
oxlint-plugin-t3code/tsconfig.json created+9
......@@ -0,0 +1,9 @@
1{
2 "extends": "../tsconfig.base.json",
3 "compilerOptions": {
4 "composite": true,
5 "types": ["bun", "node"],
6 "lib": ["ESNext", "esnext.disposable"]
7 },
8 "include": ["**/*.ts"]
9}
oxlint-plugin-t3code/utils.ts created+58
......@@ -0,0 +1,58 @@
1import type { ESTree } from "@oxlint/plugins";
2import * as Option from "effect/Option";
3
4type ExpressionWrapper =
5 | ESTree.ChainExpression
6 | ESTree.ParenthesizedExpression
7 | ESTree.TSNonNullExpression
8 | ESTree.TSAsExpression
9 | ESTree.TSTypeAssertion;
10
11type AstNode = ESTree.Node;
12
13const asAstNode = (node: unknown): Option.Option<AstNode> =>
14 typeof node === "object" && node !== null && "type" in node && typeof node.type === "string"
15 ? Option.some(node as AstNode)
16 : Option.none();
17
18const isExpressionWrapper = (node: AstNode): node is ExpressionWrapper =>
19 node.type === "ChainExpression" ||
20 node.type === "ParenthesizedExpression" ||
21 node.type === "TSNonNullExpression" ||
22 node.type === "TSAsExpression" ||
23 node.type === "TSTypeAssertion";
24
25export function unwrapExpression(node: unknown): Option.Option<AstNode> {
26 let current = asAstNode(node);
27
28 while (Option.isSome(current) && isExpressionWrapper(current.value)) {
29 current = asAstNode(current.value.expression);
30 }
31
32 return current;
33}
34
35export function getPropertyName(node: unknown): Option.Option<string> {
36 return Option.flatMap(asAstNode(node), (expression) => {
37 if (expression.type === "Identifier" && typeof expression.name === "string") {
38 return Option.some(expression.name);
39 }
40 if (expression.type === "PrivateIdentifier" && typeof expression.name === "string") {
41 return Option.some(expression.name);
42 }
43 if (expression.type === "Literal" && typeof expression.value === "string") {
44 return Option.some(expression.value);
45 }
46 return Option.none();
47 });
48}
49
50export function isIdentifier(node: Option.Option<AstNode>, name?: string): boolean {
51 if (Option.isNone(node)) return false;
52 const expression = node.value;
53 return (
54 expression.type === "Identifier" &&
55 typeof expression.name === "string" &&
56 (name === undefined || expression.name === name)
57 );
58}
package.json+3-1
......@@ -4,6 +4,7 @@
44 "workspaces": {
55 "packages": [
66 "apps/*",
7 "oxlint-plugin-t3code",
78 "packages/*",
89 "scripts"
910 ],
......@@ -61,9 +62,10 @@
6162 },
6263 "devDependencies": {
6364 "@effect/language-service": "catalog:",
65 "@oxlint/plugins": "^1.63.0",
6466 "@types/node": "catalog:",
6567 "oxfmt": "^0.40.0",
66 "oxlint": "^1.55.0",
68 "oxlint": "^1.63.0",
6769 "turbo": "^2.3.3",
6870 "typescript": "catalog:",
6971 "vitest": "catalog:"
packages/contracts/src/orchestration.test.ts+2-1
......@@ -38,6 +38,7 @@ const decodeThreadTurnStartRequestedPayload = Schema.decodeUnknownEffect(
3838const decodeOrchestrationLatestTurn = Schema.decodeUnknownEffect(OrchestrationLatestTurn);
3939const decodeOrchestrationProposedPlan = Schema.decodeUnknownEffect(OrchestrationProposedPlan);
4040const decodeOrchestrationSession = Schema.decodeUnknownEffect(OrchestrationSession);
41const encodeThreadCreatedPayload = Schema.encodeEffect(ThreadCreatedPayload);
4142
4243function getOptionValue(
4344 options: ReadonlyArray<{ id: string; value: unknown }> | undefined,
......@@ -485,7 +486,7 @@ it.effect(
485486 updatedAt: "2026-01-01T00:00:00.000Z",
486487 });
487488
488 const encoded = yield* Schema.encodeEffect(ThreadCreatedPayload)(decoded);
489 const encoded = yield* encodeThreadCreatedPayload(decoded);
489490 assert.deepStrictEqual(encoded.modelSelection.options, [{ id: "fastMode", value: true }]);
490491 }),
491492);
packages/contracts/src/settings.ts+1
......@@ -279,6 +279,7 @@ export const CursorSettings = makeProviderSettingsSchema(
279279 },
280280);
281281export type CursorSettings = typeof CursorSettings.Type;
282
282283export const OpenCodeSettings = makeProviderSettingsSchema(
283284 {
284285 enabled: Schema.Boolean.pipe(
packages/effect-acp/scripts/generate.ts+21-13
......@@ -28,15 +28,24 @@ interface GeneratedPaths {
2828 readonly metaOutputPath: string;
2929}
3030
31const UpstreamJsonSchemaSchema = Schema.Struct({
32 $defs: Schema.Record(Schema.String, Schema.Json),
33});
3134const MetaJsonSchema = Schema.Struct({
3235 agentMethods: Schema.Record(Schema.String, Schema.String),
3336 clientMethods: Schema.Record(Schema.String, Schema.String),
3437 version: Schema.Union([Schema.Number, Schema.String]),
3538});
39const encodeAgentMethods = Schema.encodeEffect(
40 Schema.fromJsonString(MetaJsonSchema.fields.agentMethods),
41);
42const encodeClientMethods = Schema.encodeEffect(
43 Schema.fromJsonString(MetaJsonSchema.fields.clientMethods),
44);
45const encodeVersion = Schema.encodeEffect(Schema.fromJsonString(MetaJsonSchema.fields.version));
3646
37const UpstreamJsonSchemaSchema = Schema.Struct({
38 $defs: Schema.Record(Schema.String, Schema.Json),
39});
47const decodeUpstreamSchema = Schema.decodeEffect(Schema.fromJsonString(UpstreamJsonSchemaSchema));
48const decodeMetaJson = Schema.decodeEffect(Schema.fromJsonString(MetaJsonSchema));
4049
4150const getGeneratedPaths = Effect.fn("getGeneratedPaths")(function* () {
4251 const path = yield* Path.Path;
......@@ -86,12 +95,9 @@ const downloadSchemas = Effect.fn("downloadSchemas")(function* (tag: string) {
8695 );
8796});
8897
89const readJsonFile = Effect.fn("readJsonFile")(function* <
90 S extends Schema.Top & { readonly DecodingServices: never },
91>(schema: S, filePath: string) {
98const readFileString = Effect.fn("readJsonFile")(function* (filePath: string) {
9299 const fs = yield* FileSystem.FileSystem;
93 const raw = yield* fs.readFileString(filePath);
94 return yield* Schema.decodeEffect(Schema.fromJsonString(schema))(raw);
100 return yield* fs.readFileString(filePath);
95101});
96102
97103const writeGeneratedFiles = Effect.fn("writeGeneratedFiles")(function* (
......@@ -201,8 +207,10 @@ const generateSchemas = Effect.fn("generateSchemas")(function* (skipDownload: bo
201207 yield* downloadSchemas(CURRENT_SCHEMA_RELEASE);
202208 }
203209
204 const upstreamSchema = yield* readJsonFile(UpstreamJsonSchemaSchema, upstreamSchemaPath);
205 const upstreamMeta = yield* readJsonFile(MetaJsonSchema, upstreamMetaPath);
210 const upstreamSchema = yield* readFileString(upstreamSchemaPath).pipe(
211 Effect.flatMap(decodeUpstreamSchema),
212 );
213 const upstreamMeta = yield* readFileString(upstreamMetaPath).pipe(Effect.flatMap(decodeMetaJson));
206214 const normalizedDefinitions = Object.fromEntries(
207215 Object.entries(upstreamSchema.$defs).map(([name, schema]) => [
208216 name,
......@@ -245,11 +253,11 @@ const generateSchemas = Effect.fn("generateSchemas")(function* (skipDownload: bo
245253
246254 const metaOutput = [
247255 ...prelude,
248 `export const AGENT_METHODS = ${yield* Schema.encodeEffect(Schema.fromJsonString(MetaJsonSchema.fields.agentMethods))(upstreamMeta.agentMethods)} as const;`,
256 `export const AGENT_METHODS = ${yield* encodeAgentMethods(upstreamMeta.agentMethods)} as const;`,
249257 "",
250 `export const CLIENT_METHODS = ${yield* Schema.encodeEffect(Schema.fromJsonString(MetaJsonSchema.fields.clientMethods))(upstreamMeta.clientMethods)} as const;`,
258 `export const CLIENT_METHODS = ${yield* encodeClientMethods(upstreamMeta.clientMethods)} as const;`,
251259 "",
252 `export const PROTOCOL_VERSION = ${yield* Schema.encodeEffect(Schema.fromJsonString(MetaJsonSchema.fields.version))(upstreamMeta.version)} as const;`,
260 `export const PROTOCOL_VERSION = ${yield* encodeVersion(upstreamMeta.version)} as const;`,
253261 "",
254262 ].join("\n");
255263
packages/effect-acp/src/_internal/shared.ts+4-2
......@@ -5,6 +5,8 @@ import { RpcClientError } from "effect/unstable/rpc";
55
66import * as AcpSchema from "../_generated/schema.gen.ts";
77import * as AcpError from "../errors.ts";
8const isError = Schema.is(AcpSchema.Error);
9const isAcpRequestError = Schema.is(AcpError.AcpRequestError);
810
911const formatSchemaIssue = SchemaIssue.makeFormatterDefault();
1012
......@@ -20,7 +22,7 @@ export const callRpc = <A>(
2022 }),
2123 ),
2224 ),
23 Effect.catchIf(Schema.is(AcpSchema.Error), (error) =>
25 Effect.catchIf(isError, (error) =>
2426 Effect.fail(AcpError.AcpRequestError.fromProtocolError(error)),
2527 ),
2628 );
......@@ -35,7 +37,7 @@ export const runHandler = Effect.fnUntraced(function* <A, B>(
3537 }
3638 return yield* handler(payload).pipe(
3739 Effect.mapError((error) =>
38 Schema.is(AcpError.AcpRequestError)(error)
40 isAcpRequestError(error)
3941 ? error.toProtocolError()
4042 : AcpError.AcpRequestError.internalError(error.message).toProtocolError(),
4143 ),
packages/effect-acp/src/agent.test.ts+6-6
......@@ -34,6 +34,10 @@ const SessionCancelNotification = jsonRpcNotification(
3434const ExtPingNotification = jsonRpcNotification("x/ping", Schema.Struct({ count: Schema.Number }));
3535const ExtRequest = jsonRpcRequest("x/test", Schema.Struct({ hello: Schema.String }));
3636const ExtResponse = jsonRpcResponse(Schema.Struct({ ok: Schema.Boolean }));
37const decodeRequestPermissionRequest = Schema.decodeEffect(
38 Schema.fromJsonString(RequestPermissionRequest),
39);
40const decodeInitializeResponse = Schema.decodeEffect(Schema.fromJsonString(InitializeResponse));
3741
3842it.effect("effect-acp agent handles core agent requests and outbound client requests", () =>
3943 Effect.gen(function* () {
......@@ -83,9 +87,7 @@ it.effect("effect-acp agent handles core agent requests and outbound client requ
8387 })
8488 .pipe(Effect.forkScoped);
8589
86 const permissionRequest = yield* Schema.decodeEffect(
87 Schema.fromJsonString(RequestPermissionRequest),
88 )(yield* Queue.take(output));
90 const permissionRequest = yield* decodeRequestPermissionRequest(yield* Queue.take(output));
8991 assert.equal(permissionRequest.jsonrpc, "2.0");
9092 assert.equal(permissionRequest.method, "session/request_permission");
9193 assert.deepEqual(permissionRequest.params, {
......@@ -136,9 +138,7 @@ it.effect("effect-acp agent handles core agent requests and outbound client requ
136138 }),
137139 );
138140
139 const initResponse = yield* Schema.decodeEffect(Schema.fromJsonString(InitializeResponse))(
140 yield* Queue.take(output),
141 );
141 const initResponse = yield* decodeInitializeResponse(yield* Queue.take(output));
142142 assert.deepEqual(initResponse, {
143143 jsonrpc: "2.0",
144144 id: 2,
packages/effect-acp/src/protocol.test.ts+23-22
......@@ -41,6 +41,13 @@ const RequestPermissionRequest = jsonRpcRequest(
4141const RequestPermissionResponse = jsonRpcResponse(AcpSchema.RequestPermissionResponse);
4242const ExtRequest = jsonRpcRequest("x/test", Schema.Struct({ hello: Schema.String }));
4343const ExtResponse = jsonRpcResponse(Schema.Struct({ ok: Schema.Boolean }));
44const decodeSessionCancelNotification = Schema.decodeEffect(
45 Schema.fromJsonString(SessionCancelNotification),
46);
47const decodeExtRequest = Schema.decodeEffect(Schema.fromJsonString(ExtRequest));
48const decodeRequestPermissionResponse = Schema.decodeEffect(
49 Schema.fromJsonString(RequestPermissionResponse),
50);
4451const mockPeerPath = Effect.map(Effect.service(Path.Path), (path) =>
4552 path.join(import.meta.dirname, "../test/fixtures/acp-mock-peer.ts"),
4653);
......@@ -79,16 +86,13 @@ it.layer(NodeServices.layer)("effect-acp protocol", (it) => {
7986
8087 yield* transport.notify("session/cancel", { sessionId: "session-1" });
8188 const outbound = yield* Queue.take(output);
82 assert.deepEqual(
83 yield* Schema.decodeEffect(Schema.fromJsonString(SessionCancelNotification))(outbound),
84 {
85 jsonrpc: "2.0",
86 method: "session/cancel",
87 params: {
88 sessionId: "session-1",
89 },
89 assert.deepEqual(yield* decodeSessionCancelNotification(outbound), {
90 jsonrpc: "2.0",
91 method: "session/cancel",
92 params: {
93 sessionId: "session-1",
9094 },
91 );
95 });
9296
9397 yield* Queue.offer(
9498 input,
......@@ -200,7 +204,7 @@ it.layer(NodeServices.layer)("effect-acp protocol", (it) => {
200204 .request("x/test", { hello: "world" })
201205 .pipe(Effect.forkScoped);
202206 const outbound = yield* Queue.take(output);
203 assert.deepEqual(yield* Schema.decodeEffect(Schema.fromJsonString(ExtRequest))(outbound), {
207 assert.deepEqual(yield* decodeExtRequest(outbound), {
204208 jsonrpc: "2.0",
205209 id: 1,
206210 method: "x/test",
......@@ -288,19 +292,16 @@ it.layer(NodeServices.layer)("effect-acp protocol", (it) => {
288292 });
289293
290294 const outbound = yield* Queue.take(output);
291 assert.deepEqual(
292 yield* Schema.decodeEffect(Schema.fromJsonString(RequestPermissionResponse))(outbound),
293 {
294 jsonrpc: "2.0",
295 id: 0,
296 result: {
297 outcome: {
298 outcome: "selected",
299 optionId: "allow",
300 },
295 assert.deepEqual(yield* decodeRequestPermissionResponse(outbound), {
296 jsonrpc: "2.0",
297 id: 0,
298 result: {
299 outcome: {
300 outcome: "selected",
301 optionId: "allow",
301302 },
302303 },
303 );
304 });
304305 }),
305306 );
306307
......@@ -321,7 +322,7 @@ it.layer(NodeServices.layer)("effect-acp protocol", (it) => {
321322 .request("x/test", { hello: "world" })
322323 .pipe(Effect.forkScoped);
323324 const outbound = yield* Queue.take(output);
324 assert.deepEqual(yield* Schema.decodeEffect(Schema.fromJsonString(ExtRequest))(outbound), {
325 assert.deepEqual(yield* decodeExtRequest(outbound), {
325326 jsonrpc: "2.0",
326327 id: 1,
327328 method: "x/test",
packages/effect-acp/src/protocol.ts+4-4
......@@ -16,6 +16,8 @@ import * as RpcServer from "effect/unstable/rpc/RpcServer";
1616import * as AcpSchema from "./_generated/schema.gen.ts";
1717import { CLIENT_METHODS } from "./_generated/meta.gen.ts";
1818import * as AcpError from "./errors.ts";
19const isAcpError = Schema.is(AcpError.AcpError);
20const isAcpRequestError = Schema.is(AcpError.AcpRequestError);
1921
2022export interface AcpProtocolLogEvent {
2123 readonly direction: "incoming" | "outgoing";
......@@ -408,7 +410,7 @@ export const makeAcpPatchedProtocol = Effect.fn("makeAcpPatchedProtocol")(functi
408410 ),
409411 Effect.matchEffect({
410412 onFailure: (error) => {
411 const normalized: AcpError.AcpError = Schema.is(AcpError.AcpError)(error)
413 const normalized: AcpError.AcpError = isAcpError(error)
412414 ? error
413415 : new AcpError.AcpTransportError({
414416 detail: error instanceof Error ? error.message : String(error),
......@@ -521,9 +523,7 @@ function isProtocolError(
521523}
522524
523525function normalizeToRequestError(error: AcpError.AcpError): AcpError.AcpRequestError {
524 return Schema.is(AcpError.AcpRequestError)(error)
525 ? error
526 : AcpError.AcpRequestError.internalError(error.message);
526 return isAcpRequestError(error) ? error : AcpError.AcpRequestError.internalError(error.message);
527527}
528528
529529function toRpcClientError(error: AcpError.AcpError): RpcClientError.RpcClientError {
packages/effect-codex-app-server/scripts/generate.ts+4-2
......@@ -38,6 +38,8 @@ const JsonSchemaDocument = Schema.StructWithRest(
3838 }),
3939 [Schema.Record(Schema.String, Schema.Json)],
4040);
41const decodeGithubContentEntries = Schema.decodeEffect(Schema.fromJsonString(GithubContentEntries));
42const decodeJsonSchemaDocument = Schema.decodeEffect(Schema.fromJsonString(JsonSchemaDocument));
4143
4244interface GeneratedPaths {
4345 readonly generatedDir: string;
......@@ -178,7 +180,7 @@ const fetchText = Effect.fn("fetchText")(function* (url: string) {
178180
179181const fetchDirectoryEntries = Effect.fn("fetchDirectoryEntries")(function* (path: string) {
180182 const raw = yield* fetchText(`${GITHUB_API_BASE}/${path}?ref=${UPSTREAM_REF}`);
181 return yield* Schema.decodeEffect(Schema.fromJsonString(GithubContentEntries))(raw);
183 return yield* decodeGithubContentEntries(raw);
182184});
183185
184186function collectSchemaEntries(
......@@ -545,7 +547,7 @@ const generateFiles = Effect.fn("generateFiles")(function* () {
545547
546548 for (const file of jsonSchemaFiles) {
547549 const raw = yield* fetchText(file.downloadUrl);
548 const parsed = yield* Schema.decodeEffect(Schema.fromJsonString(JsonSchemaDocument))(raw);
550 const parsed = yield* decodeJsonSchemaDocument(raw);
549551 const localDefinitionNames = new Map(
550552 Object.keys(parsed.definitions ?? {}).map((definitionName) => [
551553 definitionName,
packages/effect-codex-app-server/src/errors.ts+2-1
......@@ -143,9 +143,10 @@ export const CodexAppServerError = Schema.Union([
143143]);
144144
145145export type CodexAppServerError = typeof CodexAppServerError.Type;
146const isCodexAppServerRequestError = Schema.is(CodexAppServerRequestError);
146147
147148export function normalizeToRequestError(error: CodexAppServerError): CodexAppServerRequestError {
148 return Schema.is(CodexAppServerRequestError)(error)
149 return isCodexAppServerRequestError(error)
149150 ? error
150151 : CodexAppServerRequestError.internalError(error.message);
151152}
packages/effect-codex-app-server/src/protocol.test.ts+2-2
......@@ -11,11 +11,11 @@ import { assert, it } from "@effect/vitest";
1111import * as CodexError from "./errors.ts";
1212import * as CodexProtocol from "./protocol.ts";
1313import { makeInMemoryStdio } from "./_internal/stdio.ts";
14const encodeUnknownJsonString = Schema.encodeUnknownSync(Schema.UnknownFromJsonString);
1415
1516const encoder = new TextEncoder();
1617
17const encodeJsonl = (value: unknown) =>
18 encoder.encode(`${Schema.encodeUnknownSync(Schema.UnknownFromJsonString)(value)}\n`);
18const encodeJsonl = (value: unknown) => encoder.encode(`${encodeUnknownJsonString(value)}\n`);
1919
2020const decodeJson = Schema.decodeEffect(Schema.UnknownFromJsonString);
2121
packages/effect-codex-app-server/src/protocol.ts+6-3
......@@ -10,6 +10,9 @@ import * as Stream from "effect/Stream";
1010
1111import * as CodexError from "./errors.ts";
1212import { JsonRpcId, JsonRpcResponseEnvelope } from "./_internal/shared.ts";
13const isJsonRpcId = Schema.is(JsonRpcId);
14const isJsonRpcResponseEnvelope = Schema.is(JsonRpcResponseEnvelope);
15const isCodexAppServerError = Schema.is(CodexError.CodexAppServerError);
1316
1417export interface CodexAppServerProtocolLogEvent {
1518 readonly direction: "incoming" | "outgoing";
......@@ -72,7 +75,7 @@ function isIncomingRequest(value: unknown): value is CodexAppServerIncomingReque
7275 if (!isObject(value) || typeof value.method !== "string") {
7376 return false;
7477 }
75 return Schema.is(JsonRpcId)(value.id);
78 return isJsonRpcId(value.id);
7679}
7780
7881function isIncomingNotification(value: unknown): value is CodexAppServerIncomingNotification {
......@@ -80,7 +83,7 @@ function isIncomingNotification(value: unknown): value is CodexAppServerIncoming
8083}
8184
8285function isIncomingResponse(value: unknown): value is typeof JsonRpcResponseEnvelope.Type {
83 return Schema.is(JsonRpcResponseEnvelope)(value);
86 return isJsonRpcResponseEnvelope(value);
8487}
8588
8689const encodeJsonString = Schema.encodeUnknownEffect(Schema.UnknownFromJsonString);
......@@ -114,7 +117,7 @@ const decodeWireMessage = (
114117 );
115118
116119const normalizeIncomingError = (error: unknown, detail: string): CodexError.CodexAppServerError =>
117 Schema.is(CodexError.CodexAppServerError)(error)
120 isCodexAppServerError(error)
118121 ? error
119122 : new CodexError.CodexAppServerTransportError({
120123 detail,
packages/shared/src/observability.test.ts+185-180
......@@ -80,6 +80,8 @@ const makeTestLayer = (tracePath: string) =>
8080 Layer.succeed(References.MinimumLogLevel, "Info"),
8181 );
8282
83const nodeServicesIt = it.layer(NodeServices.layer);
84
8385describe("observability", () => {
8486 it("normalizes circular arrays, maps, and sets without recursing forever", () => {
8587 const array: Array<unknown> = ["alpha"];
......@@ -118,191 +120,194 @@ describe("observability", () => {
118120 );
119121 });
120122
121 it.effect("flushes buffered trace records on close", () =>
122 Effect.scoped(
123 Effect.gen(function* () {
124 const fileSystem = yield* FileSystem.FileSystem;
125 const path = yield* Path.Path;
126 const tempDir = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3-trace-sink-" });
127 const tracePath = path.join(tempDir, "shared.trace.ndjson");
128
129 const sink = yield* makeTraceSink({
130 filePath: tracePath,
131 maxBytes: 1024,
132 maxFiles: 2,
133 batchWindowMs: 10_000,
134 });
135
136 sink.push(makeRecord("alpha"));
137 sink.push(makeRecord("beta"));
138 yield* sink.close();
139
140 const lines = yield* readTraceRecords(tracePath);
141
142 assert.equal(lines.length, 2);
143 assert.equal(lines[0]?.name, "alpha");
144 assert.equal(lines[1]?.name, "beta");
145 }).pipe(Effect.provide(NodeServices.layer)),
146 ),
147 );
123 nodeServicesIt("node services", (it) => {
124 it.effect("flushes buffered trace records on close", () =>
125 Effect.scoped(
126 Effect.gen(function* () {
127 const fileSystem = yield* FileSystem.FileSystem;
128 const path = yield* Path.Path;
129 const tempDir = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3-trace-sink-" });
130 const tracePath = path.join(tempDir, "shared.trace.ndjson");
131
132 const sink = yield* makeTraceSink({
133 filePath: tracePath,
134 maxBytes: 1024,
135 maxFiles: 2,
136 batchWindowMs: 10_000,
137 });
138
139 sink.push(makeRecord("alpha"));
140 sink.push(makeRecord("beta"));
141 yield* sink.close();
142
143 const lines = yield* readTraceRecords(tracePath);
144
145 assert.equal(lines.length, 2);
146 assert.equal(lines[0]?.name, "alpha");
147 assert.equal(lines[1]?.name, "beta");
148 }),
149 ),
150 );
148151
149 it.effect("rotates the trace file when the configured max size is exceeded", () =>
150 Effect.scoped(
151 Effect.gen(function* () {
152 const fileSystem = yield* FileSystem.FileSystem;
153 const path = yield* Path.Path;
154 const tempDir = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3-trace-sink-" });
155 const tracePath = path.join(tempDir, "shared.trace.ndjson");
156
157 const sink = yield* makeTraceSink({
158 filePath: tracePath,
159 maxBytes: 180,
160 maxFiles: 2,
161 batchWindowMs: 10_000,
162 });
163
164 for (let index = 0; index < 8; index += 1) {
165 sink.push(makeRecord("rotate", `${index}-${"x".repeat(48)}`));
166 yield* sink.flush;
167 }
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)),
185 ),
186 );
152 it.effect("rotates the trace file when the configured max size is exceeded", () =>
153 Effect.scoped(
154 Effect.gen(function* () {
155 const fileSystem = yield* FileSystem.FileSystem;
156 const path = yield* Path.Path;
157 const tempDir = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3-trace-sink-" });
158 const tracePath = path.join(tempDir, "shared.trace.ndjson");
159
160 const sink = yield* makeTraceSink({
161 filePath: tracePath,
162 maxBytes: 180,
163 maxFiles: 2,
164 batchWindowMs: 10_000,
165 });
166
167 for (let index = 0; index < 8; index += 1) {
168 sink.push(makeRecord("rotate", `${index}-${"x".repeat(48)}`));
169 yield* sink.flush;
170 }
171 yield* sink.close();
172
173 const matchingFiles = (yield* fileSystem.readDirectory(tempDir))
174 .filter(
175 (entry) =>
176 entry === "shared.trace.ndjson" || entry.startsWith("shared.trace.ndjson."),
177 )
178 .toSorted();
179
180 assert.equal(
181 matchingFiles.some((entry) => entry === "shared.trace.ndjson.1"),
182 true,
183 );
184 assert.equal(
185 matchingFiles.some((entry) => entry === "shared.trace.ndjson.3"),
186 false,
187 );
188 }),
189 ),
190 );
187191
188 it.effect("drops only the invalid trace record when serialization fails", () =>
189 Effect.scoped(
190 Effect.gen(function* () {
191 const fileSystem = yield* FileSystem.FileSystem;
192 const path = yield* Path.Path;
193 const tempDir = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3-trace-sink-" });
194 const tracePath = path.join(tempDir, "shared.trace.ndjson");
195
196 const sink = yield* makeTraceSink({
197 filePath: tracePath,
198 maxBytes: 1024,
199 maxFiles: 2,
200 batchWindowMs: 10_000,
201 });
202
203 const circular: Array<unknown> = [];
204 circular.push(circular);
205
206 sink.push(makeRecord("alpha"));
207 sink.push({
208 ...makeRecord("invalid"),
209 attributes: {
210 circular,
211 },
212 } as TraceRecord);
213 sink.push(makeRecord("beta"));
214 yield* sink.close();
215
216 const lines = yield* readTraceRecords(tracePath);
217
218 assert.deepStrictEqual(
219 lines.map((line) => line.name),
220 ["alpha", "beta"],
221 );
222 }).pipe(Effect.provide(NodeServices.layer)),
223 ),
224 );
192 it.effect("drops only the invalid trace record when serialization fails", () =>
193 Effect.scoped(
194 Effect.gen(function* () {
195 const fileSystem = yield* FileSystem.FileSystem;
196 const path = yield* Path.Path;
197 const tempDir = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3-trace-sink-" });
198 const tracePath = path.join(tempDir, "shared.trace.ndjson");
199
200 const sink = yield* makeTraceSink({
201 filePath: tracePath,
202 maxBytes: 1024,
203 maxFiles: 2,
204 batchWindowMs: 10_000,
205 });
206
207 const circular: Array<unknown> = [];
208 circular.push(circular);
209
210 sink.push(makeRecord("alpha"));
211 sink.push({
212 ...makeRecord("invalid"),
213 attributes: {
214 circular,
215 },
216 } as TraceRecord);
217 sink.push(makeRecord("beta"));
218 yield* sink.close();
219
220 const lines = yield* readTraceRecords(tracePath);
221
222 assert.deepStrictEqual(
223 lines.map((line) => line.name),
224 ["alpha", "beta"],
225 );
226 }),
227 ),
228 );
225229
226 it.effect("writes nested spans to disk and captures log messages as span events", () =>
227 Effect.scoped(
228 Effect.gen(function* () {
229 const fileSystem = yield* FileSystem.FileSystem;
230 const path = yield* Path.Path;
231 const tempDir = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3-local-tracer-" });
232 const tracePath = path.join(tempDir, "shared.trace.ndjson");
233
234 yield* Effect.scoped(
235 Effect.gen(function* () {
236 const program = Effect.gen(function* () {
237 yield* Effect.annotateCurrentSpan({
238 "demo.parent": true,
239 });
240 yield* Effect.logInfo("parent event");
241 yield* Effect.gen(function* () {
230 it.effect("writes nested spans to disk and captures log messages as span events", () =>
231 Effect.scoped(
232 Effect.gen(function* () {
233 const fileSystem = yield* FileSystem.FileSystem;
234 const path = yield* Path.Path;
235 const tempDir = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3-local-tracer-" });
236 const tracePath = path.join(tempDir, "shared.trace.ndjson");
237
238 yield* Effect.scoped(
239 Effect.gen(function* () {
240 const program = Effect.gen(function* () {
242241 yield* Effect.annotateCurrentSpan({
243 "demo.child": true,
242 "demo.parent": true,
244243 });
245 yield* Effect.logInfo("child event");
246 }).pipe(Effect.withSpan("child-span"));
247 }).pipe(Effect.withSpan("parent-span"));
248
249 yield* program.pipe(Effect.provide(makeTestLayer(tracePath)));
250 }),
251 );
252
253 const records = yield* readTraceRecords(tracePath);
254 assert.equal(records.length, 2);
255
256 const parent = records.find((record) => record.name === "parent-span");
257 const child = records.find((record) => record.name === "child-span");
258
259 assert.notEqual(parent, undefined);
260 assert.notEqual(child, undefined);
261 if (!parent || !child) {
262 return;
263 }
264
265 assert.equal(child.parentSpanId, parent.spanId);
266 assert.equal(parent.attributes["demo.parent"], true);
267 assert.equal(child.attributes["demo.child"], true);
268 assert.equal(
269 parent.events.some((event) => event.name === "parent event"),
270 true,
271 );
272 assert.equal(
273 child.events.some((event) => event.name === "child event"),
274 true,
275 );
276 assert.equal(
277 child.events.some((event) => event.attributes["effect.logLevel"] === "INFO"),
278 true,
279 );
280 }).pipe(Effect.provide(NodeServices.layer)),
281 ),
282 );
244 yield* Effect.logInfo("parent event");
245 yield* Effect.gen(function* () {
246 yield* Effect.annotateCurrentSpan({
247 "demo.child": true,
248 });
249 yield* Effect.logInfo("child event");
250 }).pipe(Effect.withSpan("child-span"));
251 }).pipe(Effect.withSpan("parent-span"));
252
253 yield* program.pipe(Effect.provide(makeTestLayer(tracePath)));
254 }),
255 );
256
257 const records = yield* readTraceRecords(tracePath);
258 assert.equal(records.length, 2);
259
260 const parent = records.find((record) => record.name === "parent-span");
261 const child = records.find((record) => record.name === "child-span");
262
263 assert.notEqual(parent, undefined);
264 assert.notEqual(child, undefined);
265 if (!parent || !child) {
266 return;
267 }
268
269 assert.equal(child.parentSpanId, parent.spanId);
270 assert.equal(parent.attributes["demo.parent"], true);
271 assert.equal(child.attributes["demo.child"], true);
272 assert.equal(
273 parent.events.some((event) => event.name === "parent event"),
274 true,
275 );
276 assert.equal(
277 child.events.some((event) => event.name === "child event"),
278 true,
279 );
280 assert.equal(
281 child.events.some((event) => event.attributes["effect.logLevel"] === "INFO"),
282 true,
283 );
284 }),
285 ),
286 );
283287
284 it.effect("serializes interrupted spans with an interrupted exit status", () =>
285 Effect.scoped(
286 Effect.gen(function* () {
287 const fileSystem = yield* FileSystem.FileSystem;
288 const path = yield* Path.Path;
289 const tempDir = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3-local-tracer-" });
290 const tracePath = path.join(tempDir, "shared.trace.ndjson");
291
292 yield* Effect.scoped(
293 Effect.exit(
294 Effect.interrupt.pipe(
295 Effect.withSpan("interrupt-span"),
296 Effect.provide(makeTestLayer(tracePath)),
288 it.effect("serializes interrupted spans with an interrupted exit status", () =>
289 Effect.scoped(
290 Effect.gen(function* () {
291 const fileSystem = yield* FileSystem.FileSystem;
292 const path = yield* Path.Path;
293 const tempDir = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3-local-tracer-" });
294 const tracePath = path.join(tempDir, "shared.trace.ndjson");
295
296 yield* Effect.scoped(
297 Effect.exit(
298 Effect.interrupt.pipe(
299 Effect.withSpan("interrupt-span"),
300 Effect.provide(makeTestLayer(tracePath)),
301 ),
297302 ),
298 ),
299 );
300
301 const records = yield* readTraceRecords(tracePath);
302 assert.equal(records.length, 1);
303 assert.equal(records[0]?.name, "interrupt-span");
304 assert.equal(records[0]?.exit?._tag, "Interrupted");
305 }).pipe(Effect.provide(NodeServices.layer)),
306 ),
307 );
303 );
304
305 const records = yield* readTraceRecords(tracePath);
306 assert.equal(records.length, 1);
307 assert.equal(records[0]?.name, "interrupt-span");
308 assert.equal(records[0]?.exit?._tag, "Interrupted");
309 }),
310 ),
311 );
312 });
308313});
packages/shared/src/serverSettings.ts+6-5
......@@ -1,10 +1,12 @@
11import { ServerSettings, type ServerSettingsPatch } from "@t3tools/contracts";
2import * as Option from "effect/Option";
23import * as Schema from "effect/Schema";
34import { deepMerge } from "./Struct.ts";
45import { fromLenientJson } from "./schemaJson.ts";
56import { createModelSelection } from "./model.ts";
67
78const ServerSettingsJson = fromLenientJson(ServerSettings);
9const decodeServerSettingsJson = Schema.decodeUnknownOption(ServerSettingsJson);
810
911export interface PersistedServerObservabilitySettings {
1012 readonly otlpTracesUrl: string | undefined;
......@@ -33,12 +35,11 @@ export function extractPersistedServerObservabilitySettings(input: {
3335export function parsePersistedServerObservabilitySettings(
3436 raw: string,
3537): PersistedServerObservabilitySettings {
36 try {
37 const decoded = Schema.decodeUnknownSync(ServerSettingsJson)(raw);
38 return extractPersistedServerObservabilitySettings(decoded);
39 } catch {
40 return { otlpTracesUrl: undefined, otlpMetricsUrl: undefined };
38 const decoded = decodeServerSettingsJson(raw);
39 if (Option.isSome(decoded)) {
40 return extractPersistedServerObservabilitySettings(decoded.value);
4141 }
42 return { otlpTracesUrl: undefined, otlpMetricsUrl: undefined };
4243}
4344
4445function shouldReplaceTextGenerationModelSelection(
scripts/release-smoke.ts+1
......@@ -24,6 +24,7 @@ const workspaceFiles = [
2424 "apps/desktop/package.json",
2525 "apps/web/package.json",
2626 "apps/marketing/package.json",
27 "oxlint-plugin-t3code/package.json",
2728 "packages/client-runtime/package.json",
2829 "packages/contracts/package.json",
2930 "packages/shared/package.json",
scripts/update-release-package-versions.ts+3-2
......@@ -26,7 +26,7 @@ interface UpdateReleasePackageVersionsOptions {
2626const PackageJsonSchema = Schema.Record(Schema.String, Schema.Unknown);
2727const PackageJsonPrettyJson = fromJsonStringPretty(PackageJsonSchema);
2828const decodePackageJson = Schema.decodeUnknownEffect(PackageJsonPrettyJson);
29const encodePackageJson = Schema.encodeSync(PackageJsonPrettyJson);
29const encodePackageJson = Schema.encodeEffect(PackageJsonPrettyJson);
3030
3131export const updateReleasePackageVersions = Effect.fn("updateReleasePackageVersions")(function* (
3232 version: string,
......@@ -44,7 +44,8 @@ export const updateReleasePackageVersions = Effect.fn("updateReleasePackageVersi
4444 continue;
4545 }
4646
47 yield* fs.writeFileString(filePath, `${encodePackageJson({ ...packageJson, version })}\n`);
47 const packageJsonString = yield* encodePackageJson({ ...packageJson, version });
48 yield* fs.writeFileString(filePath, `${packageJsonString}\n`);
4849 changed = true;
4950 }
5051