authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-09-24 19:07:01-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-09-24 19:07:01-07:00
log568c9bc4d0ef0dddf74d813619c9877bd35624d7
tree3da47ab6e2055fa589b848c137d72096e5e2095b
parent3e2370fbbf1efacb341a68e6f28efb06b695e148
signature Signed by PGP key B5690EEEBB952194

chore: clear Effect language service suggestions (#13536)

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

174 files changed, 1152 insertions(+), 1311 deletions(-)

apps/desktop/src/app/DesktopAppIdentity.test.ts+1-1
...@@ -76,7 +76,7 @@ const makeAssetsLayer = (png: Option.Option<string>) =>...@@ -76,7 +76,7 @@ const makeAssetsLayer = (png: Option.Option<string>) =>
76 icns: Option.none(),76 icns: Option.none(),
77 png,77 png,
78 }),78 }),
79 resolveResourcePath: () => Effect.succeed(Option.none()),79 resolveResourcePath: () => Effect.succeedNone,
80 } satisfies DesktopAssets.DesktopAssets["Service"]);80 } satisfies DesktopAssets.DesktopAssets["Service"]);
8181
82const makeEnvironmentLayer = (overrides: TestEnvironmentInput = {}) => {82const makeEnvironmentLayer = (overrides: TestEnvironmentInput = {}) => {
apps/desktop/src/app/DesktopConnectionCatalogStore.test.ts+1-1
...@@ -46,7 +46,7 @@ function makeSafeStorageLayer(available: boolean, failDecrypt: Ref.Ref<boolean>...@@ -46,7 +46,7 @@ function makeSafeStorageLayer(available: boolean, failDecrypt: Ref.Ref<boolean>
46 return decoded.slice("encrypted:".length);46 return decoded.slice("encrypted:".length);
47 });47 });
48 },48 },
49 selectedStorageBackend: Effect.succeed(Option.none()),49 selectedStorageBackend: Effect.succeedNone,
50 } satisfies ElectronSafeStorage.ElectronSafeStorage["Service"]);50 } satisfies ElectronSafeStorage.ElectronSafeStorage["Service"]);
51}51}
5252
apps/desktop/src/app/DesktopConnectionCatalogStore.ts+1-1
...@@ -205,7 +205,7 @@ const readDocument = (...@@ -205,7 +205,7 @@ const readDocument = (
205 raw === null205 raw === null
206 ? Effect.succeed(Option.none<EncryptedConnectionCatalogDocument>())206 ? Effect.succeed(Option.none<EncryptedConnectionCatalogDocument>())
207 : decodeEncryptedConnectionCatalogDocumentJson(raw).pipe(207 : decodeEncryptedConnectionCatalogDocumentJson(raw).pipe(
208 Effect.map(Option.some),208 Effect.asSome,
209 Effect.mapError(209 Effect.mapError(
210 (cause) =>210 (cause) =>
211 new DesktopConnectionCatalogStoreDocumentDecodeError({211 new DesktopConnectionCatalogStoreDocumentDecodeError({
apps/desktop/src/backend/DesktopBackendConfiguration.ts+2-2
...@@ -199,11 +199,11 @@ const readPersistedBackendObservabilitySettings = Effect.gen(function* () {...@@ -199,11 +199,11 @@ const readPersistedBackendObservabilitySettings = Effect.gen(function* () {
199 const fileSystem = yield* FileSystem.FileSystem;199 const fileSystem = yield* FileSystem.FileSystem;
200 const environment = yield* DesktopEnvironment.DesktopEnvironment;200 const environment = yield* DesktopEnvironment.DesktopEnvironment;
201 const raw = yield* fileSystem.readFileString(environment.serverSettingsPath).pipe(201 const raw = yield* fileSystem.readFileString(environment.serverSettingsPath).pipe(
202 Effect.map(Option.some),202 Effect.asSome,
203 Effect.catchTags({203 Effect.catchTags({
204 PlatformError: (cause) =>204 PlatformError: (cause) =>
205 cause.reason._tag === "NotFound"205 cause.reason._tag === "NotFound"
206 ? Effect.succeed(Option.none())206 ? Effect.succeedNone
207 : logBackendObservabilitySettingsReadFailure(environment.serverSettingsPath, cause).pipe(207 : logBackendObservabilitySettingsReadFailure(environment.serverSettingsPath, cause).pipe(
208 Effect.as(Option.none()),208 Effect.as(Option.none()),
209 ),209 ),
apps/desktop/src/backend/DesktopBackendManager.test.ts+2-2
...@@ -162,7 +162,7 @@ function makeTestInstance(input: MakeInstanceInput) {...@@ -162,7 +162,7 @@ function makeTestInstance(input: MakeInstanceInput) {
162 forInstance: () => Effect.succeed(stubLog),162 forInstance: () => Effect.succeed(stubLog),
163 } satisfies DesktopObservability.DesktopBackendOutputLogFactory["Service"]),163 } satisfies DesktopObservability.DesktopBackendOutputLogFactory["Service"]),
164 Layer.succeed(DesktopTelemetryPublisher.DesktopTelemetryPublisher, {164 Layer.succeed(DesktopTelemetryPublisher.DesktopTelemetryPublisher, {
165 latest: Effect.succeed(Option.none()),165 latest: Effect.succeedNone,
166 changes: Stream.empty,166 changes: Stream.empty,
167 encoded: input.desktopTelemetryStream ?? Stream.empty,167 encoded: input.desktopTelemetryStream ?? Stream.empty,
168 handleControlForSource: () => Effect.void,168 handleControlForSource: () => Effect.void,
...@@ -1545,7 +1545,7 @@ describe("DesktopBackendManager", () => {...@@ -1545,7 +1545,7 @@ describe("DesktopBackendManager", () => {
15451545
1546 const mockPool = Layer.succeed(DesktopBackendPool.DesktopBackendPool, {1546 const mockPool = Layer.succeed(DesktopBackendPool.DesktopBackendPool, {
1547 list: Effect.succeed([instance1, instance2]),1547 list: Effect.succeed([instance1, instance2]),
1548 get: () => Effect.succeed(Option.none()),1548 get: () => Effect.succeedNone,
1549 primary: Effect.die(new Error("primary not implemented")),1549 primary: Effect.die(new Error("primary not implemented")),
1550 register: () => Effect.die(new Error("register not implemented")),1550 register: () => Effect.die(new Error("register not implemented")),
1551 unregister: () => Effect.die(new Error("unregister not implemented")),1551 unregister: () => Effect.die(new Error("unregister not implemented")),
apps/desktop/src/backend/DesktopBackendPool.test.ts+2-4
...@@ -67,7 +67,7 @@ function makePoolLayer(...@@ -67,7 +67,7 @@ function makePoolLayer(
67 } satisfies DesktopObservability.DesktopBackendOutputLogShape),67 } satisfies DesktopObservability.DesktopBackendOutputLogShape),
68 } satisfies DesktopObservability.DesktopBackendOutputLogFactory["Service"]),68 } satisfies DesktopObservability.DesktopBackendOutputLogFactory["Service"]),
69 Layer.succeed(DesktopTelemetryPublisher.DesktopTelemetryPublisher, {69 Layer.succeed(DesktopTelemetryPublisher.DesktopTelemetryPublisher, {
70 latest: Effect.succeed(Option.none()),70 latest: Effect.succeedNone,
71 changes: Stream.empty,71 changes: Stream.empty,
72 encoded: Stream.empty,72 encoded: Stream.empty,
73 handleControlForSource: () => Effect.void,73 handleControlForSource: () => Effect.void,
...@@ -135,9 +135,7 @@ describe("DesktopBackendPool", () => {...@@ -135,9 +135,7 @@ describe("DesktopBackendPool", () => {
135135
136 it.effect("layerTest dies when no instances are supplied", () =>136 it.effect("layerTest dies when no instances are supplied", () =>
137 Effect.exit(137 Effect.exit(
138 Effect.gen(function* () {138 DesktopBackendPool.DesktopBackendPool.pipe(Effect.provide(DesktopBackendPool.layerTest([]))),
139 yield* DesktopBackendPool.DesktopBackendPool;
140 }).pipe(Effect.provide(DesktopBackendPool.layerTest([]))),
141 ).pipe(Effect.map((exit) => assert.equal(exit._tag, "Failure"))),139 ).pipe(Effect.map((exit) => assert.equal(exit._tag, "Failure"))),
142 );140 );
143141
apps/desktop/src/backend/DesktopLocalEnvironmentAuth.test.ts+1-2
...@@ -1,7 +1,6 @@...@@ -1,7 +1,6 @@
1import { assert, describe, it } from "@effect/vitest";1import { assert, describe, it } from "@effect/vitest";
2import * as Effect from "effect/Effect";2import * as Effect from "effect/Effect";
3import * as Layer from "effect/Layer";3import * as Layer from "effect/Layer";
4import * as Option from "effect/Option";
5import * as Ref from "effect/Ref";4import * as Ref from "effect/Ref";
6import * as HttpClient from "effect/unstable/http/HttpClient";5import * as HttpClient from "effect/unstable/http/HttpClient";
7import * as HttpClientResponse from "effect/unstable/http/HttpClientResponse";6import * as HttpClientResponse from "effect/unstable/http/HttpClientResponse";
...@@ -60,7 +59,7 @@ describe("DesktopLocalEnvironmentAuth", () => {...@@ -60,7 +59,7 @@ describe("DesktopLocalEnvironmentAuth", () => {
60 {59 {
61 id: PRIMARY_LOCAL_ENVIRONMENT_ID,60 id: PRIMARY_LOCAL_ENVIRONMENT_ID,
62 label: Effect.succeed("Windows"),61 label: Effect.succeed("Windows"),
63 currentConfig: Effect.succeed(Option.some(config)),62 currentConfig: Effect.succeedSome(config),
64 },63 },
65 ]),64 ]),
66 } as unknown as DesktopBackendPool.DesktopBackendPool["Service"]);65 } as unknown as DesktopBackendPool.DesktopBackendPool["Service"]);
apps/desktop/src/electron/ElectronMenu.ts+1-1
...@@ -226,7 +226,7 @@ export const make = Effect.gen(function* () {...@@ -226,7 +226,7 @@ export const make = Effect.gen(function* () {
226 Effect.callback<Option.Option<string>>((resume) => {226 Effect.callback<Option.Option<string>>((resume) => {
227 const normalizedItems = normalizeContextMenuItems(input.items);227 const normalizedItems = normalizeContextMenuItems(input.items);
228 if (normalizedItems.length === 0) {228 if (normalizedItems.length === 0) {
229 resume(Effect.succeed(Option.none()));229 resume(Effect.succeedNone);
230 return;230 return;
231 }231 }
232232
apps/desktop/src/ipc/methods/snapShot.test.ts+13-15
...@@ -51,7 +51,7 @@ describe("window capture IPC", () => {...@@ -51,7 +51,7 @@ describe("window capture IPC", () => {
51 Effect.provide(51 Effect.provide(
52 Layer.mergeAll(52 Layer.mergeAll(
53 Layer.succeed(ElectronWindow.ElectronWindow, {53 Layer.succeed(ElectronWindow.ElectronWindow, {
54 main: Effect.succeed(Option.some({ webContents: { id: 7 } })),54 main: Effect.succeedSome({ webContents: { id: 7 } }),
55 } as ElectronWindow.ElectronWindow["Service"]),55 } as ElectronWindow.ElectronWindow["Service"]),
56 Layer.succeed(DesktopSnapShot.DesktopSnapShot, {56 Layer.succeed(DesktopSnapShot.DesktopSnapShot, {
57 previewConfig: () =>57 previewConfig: () =>
...@@ -87,7 +87,7 @@ describe("window capture IPC", () => {...@@ -87,7 +87,7 @@ describe("window capture IPC", () => {
87 Effect.provide(87 Effect.provide(
88 Layer.mergeAll(88 Layer.mergeAll(
89 Layer.succeed(ElectronWindow.ElectronWindow, {89 Layer.succeed(ElectronWindow.ElectronWindow, {
90 main: Effect.succeed(Option.some({ webContents: { id: 7 } })),90 main: Effect.succeedSome({ webContents: { id: 7 } }),
91 } as ElectronWindow.ElectronWindow["Service"]),91 } as ElectronWindow.ElectronWindow["Service"]),
92 Layer.succeed(DesktopSnapShot.DesktopSnapShot, {92 Layer.succeed(DesktopSnapShot.DesktopSnapShot, {
93 state: Effect.succeed({93 state: Effect.succeed({
...@@ -120,7 +120,7 @@ describe("window capture IPC", () => {...@@ -120,7 +120,7 @@ describe("window capture IPC", () => {
120 Effect.provide(120 Effect.provide(
121 Layer.mergeAll(121 Layer.mergeAll(
122 Layer.succeed(ElectronWindow.ElectronWindow, {122 Layer.succeed(ElectronWindow.ElectronWindow, {
123 main: Effect.succeed(Option.some({ webContents: { id: 7 } })),123 main: Effect.succeedSome({ webContents: { id: 7 } }),
124 } as ElectronWindow.ElectronWindow["Service"]),124 } as ElectronWindow.ElectronWindow["Service"]),
125 Layer.succeed(DesktopSnapShot.DesktopSnapShot, {125 Layer.succeed(DesktopSnapShot.DesktopSnapShot, {
126 state: Effect.succeed({ linuxBackend: "niri" }),126 state: Effect.succeed({ linuxBackend: "niri" }),
...@@ -156,13 +156,11 @@ describe("window capture IPC", () => {...@@ -156,13 +156,11 @@ describe("window capture IPC", () => {
156 Layer.succeed(156 Layer.succeed(
157 ElectronWindow.ElectronWindow,157 ElectronWindow.ElectronWindow,
158 ElectronWindow.ElectronWindow.of({158 ElectronWindow.ElectronWindow.of({
159 main: Effect.succeed(159 main: Effect.succeedSome({
160 Option.some({160 getBounds: () => ({ x: 100, y: 80, width: 1_000, height: 700 }),
161 getBounds: () => ({ x: 100, y: 80, width: 1_000, height: 700 }),161 getContentBounds: () => ({ x: 100, y: 118, width: 1_000, height: 662 }),
162 getContentBounds: () => ({ x: 100, y: 118, width: 1_000, height: 662 }),162 webContents,
163 webContents,163 }),
164 }),
165 ),
166 } as ElectronWindow.ElectronWindow["Service"]),164 } as ElectronWindow.ElectronWindow["Service"]),
167 ),165 ),
168 Layer.succeed(166 Layer.succeed(
...@@ -220,7 +218,7 @@ describe("window capture IPC", () => {...@@ -220,7 +218,7 @@ describe("window capture IPC", () => {
220 Layer.succeed(218 Layer.succeed(
221 ElectronWindow.ElectronWindow,219 ElectronWindow.ElectronWindow,
222 ElectronWindow.ElectronWindow.of({220 ElectronWindow.ElectronWindow.of({
223 main: Effect.succeed(Option.some({ webContents })),221 main: Effect.succeedSome({ webContents }),
224 } as ElectronWindow.ElectronWindow["Service"]),222 } as ElectronWindow.ElectronWindow["Service"]),
225 ),223 ),
226 Layer.succeed(224 Layer.succeed(
...@@ -256,7 +254,7 @@ describe("window capture IPC", () => {...@@ -256,7 +254,7 @@ describe("window capture IPC", () => {
256 Effect.provideService(254 Effect.provideService(
257 ElectronWindow.ElectronWindow,255 ElectronWindow.ElectronWindow,
258 ElectronWindow.ElectronWindow.of({256 ElectronWindow.ElectronWindow.of({
259 main: Effect.succeed(Option.some({ webContents: { id: 7 } })),257 main: Effect.succeedSome({ webContents: { id: 7 } }),
260 } as ElectronWindow.ElectronWindow["Service"]),258 } as ElectronWindow.ElectronWindow["Service"]),
261 ),259 ),
262 Effect.provideService(DesktopSnapShot.DesktopSnapShot, null as never),260 Effect.provideService(DesktopSnapShot.DesktopSnapShot, null as never),
...@@ -277,7 +275,7 @@ describe("window capture IPC", () => {...@@ -277,7 +275,7 @@ describe("window capture IPC", () => {
277 Effect.provide(275 Effect.provide(
278 Layer.mergeAll(276 Layer.mergeAll(
279 Layer.succeed(ElectronWindow.ElectronWindow, {277 Layer.succeed(ElectronWindow.ElectronWindow, {
280 main: Effect.succeed(Option.some({ webContents: { id: 7 } })),278 main: Effect.succeedSome({ webContents: { id: 7 } }),
281 } as ElectronWindow.ElectronWindow["Service"]),279 } as ElectronWindow.ElectronWindow["Service"]),
282 Layer.succeed(DesktopSnapShot.DesktopSnapShot, {280 Layer.succeed(DesktopSnapShot.DesktopSnapShot, {
283 setup: (action: string) =>281 setup: (action: string) =>
...@@ -295,7 +293,7 @@ describe("window capture IPC", () => {...@@ -295,7 +293,7 @@ describe("window capture IPC", () => {
295 Layer.succeed(293 Layer.succeed(
296 ElectronWindow.ElectronWindow,294 ElectronWindow.ElectronWindow,
297 ElectronWindow.ElectronWindow.of({295 ElectronWindow.ElectronWindow.of({
298 main: Effect.succeed(Option.some({ webContents: { id: 7 } })),296 main: Effect.succeedSome({ webContents: { id: 7 } }),
299 } as ElectronWindow.ElectronWindow["Service"]),297 } as ElectronWindow.ElectronWindow["Service"]),
300 ),298 ),
301 Layer.succeed(299 Layer.succeed(
...@@ -320,7 +318,7 @@ describe("window capture IPC", () => {...@@ -320,7 +318,7 @@ describe("window capture IPC", () => {
320 Layer.succeed(318 Layer.succeed(
321 ElectronWindow.ElectronWindow,319 ElectronWindow.ElectronWindow,
322 ElectronWindow.ElectronWindow.of({320 ElectronWindow.ElectronWindow.of({
323 main: Effect.succeed(Option.some({ webContents: { id: 7 } })),321 main: Effect.succeedSome({ webContents: { id: 7 } }),
324 } as ElectronWindow.ElectronWindow["Service"]),322 } as ElectronWindow.ElectronWindow["Service"]),
325 ),323 ),
326 Layer.succeed(324 Layer.succeed(
apps/desktop/src/ipc/methods/sshEnvironment.ts+5-7
...@@ -137,13 +137,11 @@ export const ensureSshEnvironment = DesktopIpc.makeIpcMethod({...@@ -137,13 +137,11 @@ export const ensureSshEnvironment = DesktopIpc.makeIpcMethod({
137 }) {137 }) {
138 const sshEnvironment = yield* DesktopSshEnvironment.DesktopSshEnvironment;138 const sshEnvironment = yield* DesktopSshEnvironment.DesktopSshEnvironment;
139 return yield* sshEnvironment.ensureEnvironment(target, options).pipe(139 return yield* sshEnvironment.ensureEnvironment(target, options).pipe(
140 Effect.catch((error) =>140 Effect.catchIf(DesktopSshEnvironment.isDesktopSshPasswordPromptCancellation, (error) =>
141 DesktopSshEnvironment.isDesktopSshPasswordPromptCancellation(error)141 Effect.succeed({
142 ? Effect.succeed({142 type: DesktopSshPasswordPromptCancelledType,
143 type: DesktopSshPasswordPromptCancelledType,143 message: error.message,
144 message: error.message,144 }),
145 })
146 : Effect.fail(error),
147 ),145 ),
148 );146 );
149 }),147 }),
apps/desktop/src/ipc/methods/window.test.ts+12-14
...@@ -61,7 +61,7 @@ const defaultWslInstance: DesktopBackendManager.DesktopBackendInstance = {...@@ -61,7 +61,7 @@ const defaultWslInstance: DesktopBackendManager.DesktopBackendInstance = {
61 label: Effect.succeed("WSL (default distro)"),61 label: Effect.succeed("WSL (default distro)"),
62 start: Effect.void,62 start: Effect.void,
63 stop: () => Effect.void,63 stop: () => Effect.void,
64 currentConfig: Effect.succeed(Option.some(readyWslConfig)),64 currentConfig: Effect.succeedSome(readyWslConfig),
65 snapshot: Effect.succeed({65 snapshot: Effect.succeed({
66 desiredRunning: true,66 desiredRunning: true,
67 ready: true,67 ready: true,
...@@ -101,7 +101,7 @@ describe("getLocalEnvironmentBootstraps", () => {...@@ -101,7 +101,7 @@ describe("getLocalEnvironmentBootstraps", () => {
101 };101 };
102 const retryingInstance: DesktopBackendManager.DesktopBackendInstance = {102 const retryingInstance: DesktopBackendManager.DesktopBackendInstance = {
103 ...defaultWslInstance,103 ...defaultWslInstance,
104 currentConfig: Effect.succeed(Option.some(retryingConfig)),104 currentConfig: Effect.succeedSome(retryingConfig),
105 snapshot: Effect.succeed({105 snapshot: Effect.succeed({
106 desiredRunning: true,106 desiredRunning: true,
107 ready: false,107 ready: false,
...@@ -128,16 +128,14 @@ describe("getLocalEnvironmentBootstraps", () => {...@@ -128,16 +128,14 @@ describe("getLocalEnvironmentBootstraps", () => {
128 it.effect("omits a bounded transient bootstrap after retries stop", () => {128 it.effect("omits a bounded transient bootstrap after retries stop", () => {
129 const stoppedInstance: DesktopBackendManager.DesktopBackendInstance = {129 const stoppedInstance: DesktopBackendManager.DesktopBackendInstance = {
130 ...defaultWslInstance,130 ...defaultWslInstance,
131 currentConfig: Effect.succeed(131 currentConfig: Effect.succeedSome({
132 Option.some({132 ...readyWslConfig,
133 ...readyWslConfig,133 preflightFailure: Option.some({
134 preflightFailure: Option.some({134 reason: "WSL probe timed out",
135 reason: "WSL probe timed out",135 fatal: false,
136 fatal: false,136 retryLimit: 12,
137 retryLimit: 12,
138 }),
139 }),137 }),
140 ),138 }),
141 snapshot: Effect.succeed({139 snapshot: Effect.succeed({
142 desiredRunning: false,140 desiredRunning: false,
143 ready: false,141 ready: false,
...@@ -163,7 +161,7 @@ describe("getWindowFullscreenState", () => {...@@ -163,7 +161,7 @@ describe("getWindowFullscreenState", () => {
163 }).pipe(161 }).pipe(
164 Effect.provide(162 Effect.provide(
165 Layer.mock(ElectronWindow.ElectronWindow)({163 Layer.mock(ElectronWindow.ElectronWindow)({
166 currentMainOrFirst: Effect.succeed(Option.some(window)),164 currentMainOrFirst: Effect.succeedSome(window),
167 }),165 }),
168 ),166 ),
169 );167 );
...@@ -206,7 +204,7 @@ describe("pasteAsText", () => {...@@ -206,7 +204,7 @@ describe("pasteAsText", () => {
206 }).pipe(204 }).pipe(
207 Effect.provide(205 Effect.provide(
208 Layer.mock(ElectronWindow.ElectronWindow)({206 Layer.mock(ElectronWindow.ElectronWindow)({
209 main: Effect.succeed(Option.some(window)),207 main: Effect.succeedSome(window),
210 }),208 }),
211 ),209 ),
212 );210 );
...@@ -219,7 +217,7 @@ describe("pickProjectFavicon", () => {...@@ -219,7 +217,7 @@ describe("pickProjectFavicon", () => {
219 Layer.mergeAll(217 Layer.mergeAll(
220 Layer.mock(ElectronDialog.ElectronDialog)({ pickFiles }),218 Layer.mock(ElectronDialog.ElectronDialog)({ pickFiles }),
221 Layer.mock(ElectronWindow.ElectronWindow)({219 Layer.mock(ElectronWindow.ElectronWindow)({
222 focusedMainOrFirst: Effect.succeed(Option.none()),220 focusedMainOrFirst: Effect.succeedNone,
223 }),221 }),
224 DesktopAppSettings.layerTest(settings),222 DesktopAppSettings.layerTest(settings),
225 );223 );
apps/desktop/src/ipc/methods/wsl.test.ts+1-2
...@@ -2,7 +2,6 @@ import { DesktopWslStateSchema } from "@t3tools/contracts";...@@ -2,7 +2,6 @@ import { DesktopWslStateSchema } from "@t3tools/contracts";
2import { assert, describe, it } from "@effect/vitest";2import { assert, describe, it } from "@effect/vitest";
3import * as Effect from "effect/Effect";3import * as Effect from "effect/Effect";
4import * as Layer from "effect/Layer";4import * as Layer from "effect/Layer";
5import * as Option from "effect/Option";
6import * as Schema from "effect/Schema";5import * as Schema from "effect/Schema";
76
8import * as DesktopEnvironment from "../../app/DesktopEnvironment.ts";7import * as DesktopEnvironment from "../../app/DesktopEnvironment.ts";
...@@ -34,7 +33,7 @@ function makeWslBackendLayer(input: { readonly onReconcile?: Effect.Effect<void>...@@ -34,7 +33,7 @@ function makeWslBackendLayer(input: { readonly onReconcile?: Effect.Effect<void>
34 DesktopWslBackend.DesktopWslBackend,33 DesktopWslBackend.DesktopWslBackend,
35 DesktopWslBackend.DesktopWslBackend.of({34 DesktopWslBackend.DesktopWslBackend.of({
36 reconcile: input.onReconcile ?? Effect.void,35 reconcile: input.onReconcile ?? Effect.void,
37 lastPreflightError: Effect.succeed(Option.none()),36 lastPreflightError: Effect.succeedNone,
38 }),37 }),
39 );38 );
40}39}
apps/desktop/src/preview/BrowserImport/ChromiumKeys.ts+3-2
...@@ -315,8 +315,9 @@ export const resolveChromiumKeys = Effect.fn("ChromiumKeys.resolveChromiumKeys")...@@ -315,8 +315,9 @@ export const resolveChromiumKeys = Effect.fn("ChromiumKeys.resolveChromiumKeys")
315 // v10 remains importable when Secret Service is absent or does not315 // v10 remains importable when Secret Service is absent or does not
316 // contain a key. An explicit denial/lock/cancel remains a consent316 // contain a key. An explicit denial/lock/cancel remains a consent
317 // failure rather than being silently downgraded.317 // failure rather than being silently downgraded.
318 Effect.catch((error) =>318 Effect.catchIf(
319 error.reason === "needsKeychainApproval" ? Effect.fail(error) : Effect.succeed(error),319 (error) => error.reason !== "needsKeychainApproval",
320 (error) => Effect.succeed(error),
320 ),321 ),
321 )322 )
322 : undefined;323 : undefined;
apps/desktop/src/preview/BrowserImport/FirefoxCookies.ts+41-42
...@@ -114,56 +114,55 @@ const expiryToSeconds = (expiry: number, schemaVersion: number): number | undefi...@@ -114,56 +114,55 @@ const expiryToSeconds = (expiry: number, schemaVersion: number): number | undefi
114 return schemaVersion >= FIREFOX_EXPIRY_MILLISECONDS_SCHEMA ? Math.floor(expiry / 1000) : expiry;114 return schemaVersion >= FIREFOX_EXPIRY_MILLISECONDS_SCHEMA ? Math.floor(expiry / 1000) : expiry;
115};115};
116116
117export const readFirefoxCookies = Effect.fn("FirefoxCookies.readFirefoxCookies")(function* (117export const readFirefoxCookies = Effect.fn("FirefoxCookies.readFirefoxCookies")(
118 cookieDatabasePath: string,118 function* (cookieDatabasePath: string) {
119) {119 const snapshotPath = yield* snapshotCookieDatabase(cookieDatabasePath);
120 const snapshotPath = yield* snapshotCookieDatabase(cookieDatabasePath).pipe(
121 Effect.mapError((cause) => new FirefoxCookieReadError({ cookieDatabasePath, cause })),
122 );
123120
124 const { rows, schemaVersion } = yield* Effect.gen(function* () {121 const { rows, schemaVersion } = yield* Effect.gen(function* () {
125 const sql = yield* SqlClient.SqlClient;122 const sql = yield* SqlClient.SqlClient;
126 const [versionRow] = yield* decodeUserVersion(yield* sql`pragma user_version`);123 const [versionRow] = yield* decodeUserVersion(yield* sql`pragma user_version`);
127 const schemaVersion = versionRow?.user_version ?? 0;124 const schemaVersion = versionRow?.user_version ?? 0;
128 const hasRawSameSite =125 const hasRawSameSite =
129 schemaVersion >= FIREFOX_RAW_SAMESITE_FIRST_SCHEMA &&126 schemaVersion >= FIREFOX_RAW_SAMESITE_FIRST_SCHEMA &&
130 schemaVersion <= FIREFOX_RAW_SAMESITE_LAST_SCHEMA;127 schemaVersion <= FIREFOX_RAW_SAMESITE_LAST_SCHEMA;
131 // Only the default container. Firefox isolates cookies per container and128 // Only the default container. Firefox isolates cookies per container and
132 // per private window via `originAttributes` (`^userContextId=2`,129 // per private window via `originAttributes` (`^userContextId=2`,
133 // `^privateBrowsingId=1`); Electron has no equivalent, so importing them130 // `^privateBrowsingId=1`); Electron has no equivalent, so importing them
134 // all would collapse several identities onto one host/name/path and hand131 // all would collapse several identities onto one host/name/path and hand
135 // the profile an arbitrary container's session.132 // the profile an arbitrary container's session.
136 const raw = hasRawSameSite133 const raw = hasRawSameSite
137 ? yield* sql`134 ? yield* sql`
138 select host, name, value, path, expiry, isSecure, isHttpOnly, sameSite, rawSameSite135 select host, name, value, path, expiry, isSecure, isHttpOnly, sameSite, rawSameSite
139 from moz_cookies136 from moz_cookies
140 where originAttributes = ''137 where originAttributes = ''
141 `138 `
142 : yield* sql`139 : yield* sql`
143 select host, name, value, path, expiry, isSecure, isHttpOnly, sameSite,140 select host, name, value, path, expiry, isSecure, isHttpOnly, sameSite,
144 null as rawSameSite141 null as rawSameSite
145 from moz_cookies142 from moz_cookies
146 where originAttributes = ''143 where originAttributes = ''
147 `;144 `;
148 return { rows: yield* decodeCookieRows(raw), schemaVersion };145 return { rows: yield* decodeCookieRows(raw), schemaVersion };
149 }).pipe(146 }).pipe(Effect.provide(NodeSqliteClient.layer({ filename: snapshotPath, readonly: true })));
150 Effect.provide(NodeSqliteClient.layer({ filename: snapshotPath, readonly: true })),
151 Effect.mapError((cause) => new FirefoxCookieReadError({ cookieDatabasePath, cause })),
152 );
153147
154 return rows.map((row) => {148 return rows.map((row) => {
155 const secure = row.isSecure === 1;149 const secure = row.isSecure === 1;
156 const scope = cookieScope(row.host, row.path, secure);150 const scope = cookieScope(row.host, row.path, secure);
157 return {151 return {
158 url: scope.url,152 url: scope.url,
159 name: row.name,153 name: row.name,
160 value: row.value,154 value: row.value,
161 domain: scope.domain,155 domain: scope.domain,
162 path: row.path,156 path: row.path,
163 secure,157 secure,
164 httpOnly: row.isHttpOnly === 1,158 httpOnly: row.isHttpOnly === 1,
165 expirationDate: expiryToSeconds(row.expiry, schemaVersion),159 expirationDate: expiryToSeconds(row.expiry, schemaVersion),
166 sameSite: sameSiteFromColumn(row.sameSite, row.rawSameSite),160 sameSite: sameSiteFromColumn(row.sameSite, row.rawSameSite),
167 } satisfies ImportedCookie;161 } satisfies ImportedCookie;
168 });162 });
169});163 },
164 (effect, cookieDatabasePath) =>
165 effect.pipe(
166 Effect.mapError((cause) => new FirefoxCookieReadError({ cookieDatabasePath, cause })),
167 ),
168);
apps/desktop/src/preview/BrowserSession.ts+6-6
...@@ -231,8 +231,9 @@ export const make = Effect.gen(function* BrowserSessionMake() {...@@ -231,8 +231,9 @@ export const make = Effect.gen(function* BrowserSessionMake() {
231 getSession,231 getSession,
232 clearCookies: Effect.fn("BrowserSession.clearCookies")(function* (partitions?) {232 clearCookies: Effect.fn("BrowserSession.clearCookies")(function* (partitions?) {
233 const sessions = yield* SynchronizedRef.get(sessionsRef);233 const sessions = yield* SynchronizedRef.get(sessionsRef);
234 yield* Effect.all(234 yield* Effect.forEach(
235 selectSessions(sessions, partitions).map(([partition, browserSession]) =>235 selectSessions(sessions, partitions),
236 ([partition, browserSession]) =>
236 Effect.tryPromise({237 Effect.tryPromise({
237 try: () =>238 try: () =>
238 browserSession.clearStorageData({239 browserSession.clearStorageData({
...@@ -244,14 +245,14 @@ export const make = Effect.gen(function* BrowserSessionMake() {...@@ -244,14 +245,14 @@ export const make = Effect.gen(function* BrowserSessionMake() {
244 cause,245 cause,
245 }),246 }),
246 }),247 }),
247 ),
248 { concurrency: "unbounded", discard: true },248 { concurrency: "unbounded", discard: true },
249 );249 );
250 }),250 }),
251 clearCache: Effect.fn("BrowserSession.clearCache")(function* (partitions?) {251 clearCache: Effect.fn("BrowserSession.clearCache")(function* (partitions?) {
252 const sessions = yield* SynchronizedRef.get(sessionsRef);252 const sessions = yield* SynchronizedRef.get(sessionsRef);
253 yield* Effect.all(253 yield* Effect.forEach(
254 selectSessions(sessions, partitions).map(([partition, browserSession]) =>254 selectSessions(sessions, partitions),
255 ([partition, browserSession]) =>
255 Effect.tryPromise({256 Effect.tryPromise({
256 try: () => browserSession.clearCache(),257 try: () => browserSession.clearCache(),
257 catch: (cause) =>258 catch: (cause) =>
...@@ -260,7 +261,6 @@ export const make = Effect.gen(function* BrowserSessionMake() {...@@ -260,7 +261,6 @@ export const make = Effect.gen(function* BrowserSessionMake() {
260 cause,261 cause,
261 }),262 }),
262 }),263 }),
263 ),
264 { concurrency: "unbounded", discard: true },264 { concurrency: "unbounded", discard: true },
265 );265 );
266 }),266 }),
apps/desktop/src/preview/Manager.ts+25-28
...@@ -942,14 +942,14 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function...@@ -942,14 +942,14 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function
942 delivery: () => Effect.Effect<void>,942 delivery: () => Effect.Effect<void>,
943 ) =>943 ) =>
944 Effect.suspend(delivery).pipe(944 Effect.suspend(delivery).pipe(
945 Effect.catchCause((cause) =>945 Effect.catchCauseIf(
946 Cause.hasInterrupts(cause)946 (cause) => !Cause.hasInterrupts(cause),
947 ? Effect.failCause(cause)947 (cause) =>
948 : Effect.logWarning("Desktop preview event listener failed.", {948 Effect.logWarning("Desktop preview event listener failed.", {
949 eventKind,949 eventKind,
950 tabId,950 tabId,
951 cause,951 cause,
952 }),952 }),
953 ),953 ),
954 );954 );
955955
...@@ -1115,15 +1115,13 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function...@@ -1115,15 +1115,13 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function
1115 }1115 }
1116 return resolvedPath;1116 return resolvedPath;
1117 }).pipe(1117 }).pipe(
1118 Effect.flatMap((resolvedPath) =>1118 Effect.filterOrFail(
1119 resolvedPath === null1119 (resolvedPath) => resolvedPath !== null,
1120 ? Effect.fail(1120 () =>
1121 new PreviewArtifactPathOutsideDirectoryError({1121 new PreviewArtifactPathOutsideDirectoryError({
1122 artifactPath,1122 artifactPath,
1123 artifactDirectory: resolvedArtifactDirectory,1123 artifactDirectory: resolvedArtifactDirectory,
1124 }),1124 }),
1125 )
1126 : Effect.succeed(resolvedPath),
1127 ),1125 ),
1128 );1126 );
11291127
...@@ -1404,14 +1402,13 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function...@@ -1404,14 +1402,13 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function
1404 wcDebugger.on("message", onMessage);1402 wcDebugger.on("message", onMessage);
1405 wcDebugger.attach("1.3");1403 wcDebugger.attach("1.3");
1406 });1404 });
1407 yield* Effect.all(1405 yield* Effect.forEach(
1408 ["Runtime.enable", "Accessibility.enable", "Network.enable", "Log.enable"].map(1406 ["Runtime.enable", "Accessibility.enable", "Network.enable", "Log.enable"],
1409 (method) =>1407 (method) =>
1410 attemptPromise(1408 attemptPromise(
1411 { operation: `initializeDebugger.${method}`, webContentsId: wc.id },1409 { operation: `initializeDebugger.${method}`, webContentsId: wc.id },
1412 () => wcDebugger.sendCommand(method),1410 () => wcDebugger.sendCommand(method),
1413 ),1411 ),
1414 ),
1415 { concurrency: "unbounded", discard: true },1412 { concurrency: "unbounded", discard: true },
1416 );1413 );
1417 return [1414 return [
...@@ -4056,13 +4053,13 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function...@@ -4056,13 +4053,13 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function
4056 receiptKey: JSON.stringify(`__t3NativeKey_${NodeCrypto.randomUUID()}`),4053 receiptKey: JSON.stringify(`__t3NativeKey_${NodeCrypto.randomUUID()}`),
4057 })),4054 })),
4058 ({ frames, receiptKey }) =>4055 ({ frames, receiptKey }) =>
4059 Effect.all(4056 Effect.forEach(
4060 frames.map((frame) =>4057 frames,
4058 (frame) =>
4061 evaluate(frame, `globalThis[${receiptKey}]?.dispose()`).pipe(4059 evaluate(frame, `globalThis[${receiptKey}]?.dispose()`).pipe(
4062 Effect.timeoutOption(1_000),4060 Effect.timeoutOption(1_000),
4063 Effect.ignore,4061 Effect.ignore,
4064 ),4062 ),
4065 ),
4066 { concurrency: "unbounded", discard: true },4063 { concurrency: "unbounded", discard: true },
4067 ),4064 ),
4068 );4065 );
apps/desktop/src/settings/DesktopClientSettings.ts+2-2
...@@ -75,7 +75,7 @@ const readClientSettings = (...@@ -75,7 +75,7 @@ const readClientSettings = (
75 settingsPath: string,75 settingsPath: string,
76): Effect.Effect<Option.Option<ClientSettings>, DesktopClientSettingsReadError> =>76): Effect.Effect<Option.Option<ClientSettings>, DesktopClientSettingsReadError> =>
77 fileSystem.readFileString(settingsPath).pipe(77 fileSystem.readFileString(settingsPath).pipe(
78 Effect.map(Option.some),78 Effect.asSome,
79 Effect.catchTags({79 Effect.catchTags({
80 PlatformError: (cause) =>80 PlatformError: (cause) =>
81 cause.reason._tag === "NotFound"81 cause.reason._tag === "NotFound"
...@@ -98,7 +98,7 @@ const readClientSettings = (...@@ -98,7 +98,7 @@ const readClientSettings = (
98 onNone: () => Effect.succeed(Option.none<ClientSettings>()),98 onNone: () => Effect.succeed(Option.none<ClientSettings>()),
99 onSome: (raw) =>99 onSome: (raw) =>
100 decodeClientSettingsJson(raw).pipe(100 decodeClientSettingsJson(raw).pipe(
101 Effect.map((settings) => Option.some(settings)),101 Effect.asSome,
102 Effect.catchTags({102 Effect.catchTags({
103 SchemaError: (cause) =>103 SchemaError: (cause) =>
104 Effect.logWarning("Could not decode desktop client settings.", cause).pipe(104 Effect.logWarning("Could not decode desktop client settings.", cause).pipe(
apps/desktop/src/settings/DesktopSavedEnvironments.test.ts+1-1
...@@ -94,7 +94,7 @@ function makeSafeStorageLayer(input: {...@@ -94,7 +94,7 @@ function makeSafeStorageLayer(input: {
94 }94 }
95 return Effect.succeed(decoded.slice("enc:".length));95 return Effect.succeed(decoded.slice("enc:".length));
96 },96 },
97 selectedStorageBackend: Effect.succeed(Option.none()),97 selectedStorageBackend: Effect.succeedNone,
98 } satisfies ElectronSafeStorage.ElectronSafeStorage["Service"]);98 } satisfies ElectronSafeStorage.ElectronSafeStorage["Service"]);
99}99}
100100
apps/desktop/src/snapShot/DesktopSnapShot.ts+6-7
...@@ -820,7 +820,7 @@ export const make = Effect.gen(function* () {...@@ -820,7 +820,7 @@ export const make = Effect.gen(function* () {
820 };820 };
821821
822 const emit = (event: DesktopSnapShotEvent) =>822 const emit = (event: DesktopSnapShotEvent) =>
823 desktopWindow.dispatchSnapShotEvent(event).pipe(Effect.catchCause(() => Effect.void));823 desktopWindow.dispatchSnapShotEvent(event).pipe(Effect.ignoreCause);
824 const setFailure = (message: string, captureId?: string) =>824 const setFailure = (message: string, captureId?: string) =>
825 Ref.update(stateRef, (state) => ({ ...state, message })).pipe(825 Ref.update(stateRef, (state) => ({ ...state, message })).pipe(
826 Effect.andThen(826 Effect.andThen(
...@@ -846,10 +846,9 @@ export const make = Effect.gen(function* () {...@@ -846,10 +846,9 @@ export const make = Effect.gen(function* () {
846 const discardCapture = Effect.fn("desktop.snapShot.discardCapture")(function* (id: string) {846 const discardCapture = Effect.fn("desktop.snapShot.discardCapture")(function* (id: string) {
847 closeLinuxFeedback(id);847 closeLinuxFeedback(id);
848 transition.dismiss(id);848 transition.dismiss(id);
849 yield* Effect.all(849 yield* Effect.forEach(
850 [`${id}.png`, `${id}.tmp.png`, `${id}.json`, `${id}.json.tmp`].map((name) =>850 [`${id}.png`, `${id}.tmp.png`, `${id}.json`, `${id}.json.tmp`],
851 fileSystem.remove(path.join(captureDirectory, name), { force: true }),851 (name) => fileSystem.remove(path.join(captureDirectory, name), { force: true }),
852 ),
853 { concurrency: "unbounded", discard: true },852 { concurrency: "unbounded", discard: true },
854 ).pipe(Effect.ignore);853 ).pipe(Effect.ignore);
855 });854 });
...@@ -904,7 +903,7 @@ export const make = Effect.gen(function* () {...@@ -904,7 +903,7 @@ export const make = Effect.gen(function* () {
904 if (snapshot.animationStarted) {903 if (snapshot.animationStarted) {
905 yield* emit({ type: "started", id: id as DesktopSnapShotId });904 yield* emit({ type: "started", id: id as DesktopSnapShotId });
906 } else {905 } else {
907 yield* desktopWindow.activate.pipe(Effect.catchCause(() => Effect.void));906 yield* desktopWindow.activate.pipe(Effect.ignoreCause);
908 }907 }
909 return { id, capturedAt, ...snapshot };908 return { id, capturedAt, ...snapshot };
910 }).pipe(Effect.mapError((cause) => captureFailure(cause, id)));909 }).pipe(Effect.mapError((cause) => captureFailure(cause, id)));
...@@ -1511,7 +1510,7 @@ export const make = Effect.gen(function* () {...@@ -1511,7 +1510,7 @@ export const make = Effect.gen(function* () {
1511 null,1510 null,
1512 ),1511 ),
1513 ),1512 ),
1514 Effect.catch(() => Effect.void),1513 Effect.ignore,
1515 ),1514 ),
1516 ),1515 ),
1517 configure,1516 configure,
apps/desktop/src/ssh/DesktopSshPasswordPrompts.test.ts+3-4
...@@ -4,7 +4,6 @@ import * as Duration from "effect/Duration";...@@ -4,7 +4,6 @@ import * as Duration from "effect/Duration";
4import * as Effect from "effect/Effect";4import * as Effect from "effect/Effect";
5import * as Fiber from "effect/Fiber";5import * as Fiber from "effect/Fiber";
6import * as Layer from "effect/Layer";6import * as Layer from "effect/Layer";
7import * as Option from "effect/Option";
8import * as TestClock from "effect/testing/TestClock";7import * as TestClock from "effect/testing/TestClock";
9import type * as Electron from "electron";8import type * as Electron from "electron";
109
...@@ -92,9 +91,9 @@ function makeElectronWindowLayer(window: ReturnType<typeof makeTestWindow>["wind...@@ -92,9 +91,9 @@ function makeElectronWindowLayer(window: ReturnType<typeof makeTestWindow>["wind
92 ElectronWindow.ElectronWindow,91 ElectronWindow.ElectronWindow,
93 ElectronWindow.ElectronWindow.of({92 ElectronWindow.ElectronWindow.of({
94 create: () => Effect.die("unexpected BrowserWindow creation"),93 create: () => Effect.die("unexpected BrowserWindow creation"),
95 main: Effect.succeed(Option.some(window as Electron.BrowserWindow)),94 main: Effect.succeedSome(window as Electron.BrowserWindow),
96 currentMainOrFirst: Effect.succeed(Option.some(window as Electron.BrowserWindow)),95 currentMainOrFirst: Effect.succeedSome(window as Electron.BrowserWindow),
97 focusedMainOrFirst: Effect.succeed(Option.some(window as Electron.BrowserWindow)),96 focusedMainOrFirst: Effect.succeedSome(window as Electron.BrowserWindow),
98 setMain: () => Effect.void,97 setMain: () => Effect.void,
99 clearMain: () => Effect.void,98 clearMain: () => Effect.void,
100 prepareReveal: () => Effect.succeed(false),99 prepareReveal: () => Effect.succeed(false),
apps/desktop/src/telemetry/DesktopTelemetryPublisher.ts+7-6
...@@ -289,12 +289,12 @@ export const make = Effect.fn("desktop.telemetryPublisher.make")(function* () {...@@ -289,12 +289,12 @@ export const make = Effect.fn("desktop.telemetryPublisher.make")(function* () {
289 yield* Ref.set(latest, Option.some(snapshot));289 yield* Ref.set(latest, Option.some(snapshot));
290 yield* PubSub.publish(changes, snapshot);290 yield* PubSub.publish(changes, snapshot);
291 }).pipe(291 }).pipe(
292 Effect.catchCause((cause) =>292 Effect.catchCauseIf(
293 Cause.hasInterrupts(cause)293 (cause) => !Cause.hasInterrupts(cause),
294 ? Effect.failCause(cause)294 (cause) =>
295 : Effect.logWarning("Failed to sample Electron telemetry", {295 Effect.logWarning("Failed to sample Electron telemetry", {
296 cause: String(cause),296 cause: String(cause),
297 }),297 }),
298 ),298 ),
299 );299 );
300300
...@@ -306,6 +306,7 @@ export const make = Effect.fn("desktop.telemetryPublisher.make")(function* () {...@@ -306,6 +306,7 @@ export const make = Effect.fn("desktop.telemetryPublisher.make")(function* () {
306 Ref.get(diagnosticsDemandSources).pipe(Effect.map((sources) => sources.size > 0)),306 Ref.get(diagnosticsDemandSources).pipe(Effect.map((sources) => sources.size > 0)),
307 Ref.get(hostPowerIntervals),307 Ref.get(hostPowerIntervals),
308 ]);308 ]);
309 // @effect-diagnostics-next-line raceFirstWithSleepToTimeout:off - races a trigger queue against the interval; both arms are real outcomes, not a timeout
309 const allowSuspendRecovery = yield* Effect.raceFirst(310 const allowSuspendRecovery = yield* Effect.raceFirst(
310 Queue.take(sampleTriggers).pipe(Effect.as(false)),311 Queue.take(sampleTriggers).pipe(Effect.as(false)),
311 Effect.sleep(sampleInterval(currentPower, demand, intervals)).pipe(Effect.as(true)),312 Effect.sleep(sampleInterval(currentPower, demand, intervals)).pipe(Effect.as(true)),
apps/desktop/src/updates/DesktopUpdates.ts+16-15
...@@ -668,24 +668,25 @@ export const make = Effect.gen(function* () {...@@ -668,24 +668,25 @@ export const make = Effect.gen(function* () {
668 }),668 }),
669 ).pipe(Effect.withSpan("desktop.updates.installDownloadedUpdate"));669 ).pipe(Effect.withSpan("desktop.updates.installDownloadedUpdate"));
670670
671 const installWithExpectedVersion = (expectedVersion?: string) =>671 const installWithExpectedVersion = Effect.fn("desktop.updates.install")(function* (
672 Effect.gen(function* () {672 expectedVersion?: string,
673 if (yield* Ref.get(desktopState.quitting)) {673 ) {
674 return {674 if (yield* Ref.get(desktopState.quitting)) {
675 accepted: false,
676 completed: false,
677 failed: false,
678 state: yield* Ref.get(updateStateRef),
679 };
680 }
681 const result = yield* installDownloadedUpdate(expectedVersion);
682 return {675 return {
683 accepted: result.accepted,676 accepted: false,
684 completed: result.completed,677 completed: false,
685 failed: result.failed,678 failed: false,
686 state: yield* Ref.get(updateStateRef),679 state: yield* Ref.get(updateStateRef),
687 };680 };
688 }).pipe(Effect.withSpan("desktop.updates.install"));681 }
682 const result = yield* installDownloadedUpdate(expectedVersion);
683 return {
684 accepted: result.accepted,
685 completed: result.completed,
686 failed: result.failed,
687 state: yield* Ref.get(updateStateRef),
688 };
689 });
689690
690 const startUpdatePollers: Effect.Effect<void, never, Scope.Scope> = Effect.gen(function* () {691 const startUpdatePollers: Effect.Effect<void, never, Scope.Scope> = Effect.gen(function* () {
691 yield* Effect.sleep(AUTO_UPDATE_STARTUP_DELAY).pipe(692 yield* Effect.sleep(AUTO_UPDATE_STARTUP_DELAY).pipe(
apps/desktop/src/updates/updatesTestHarness.ts+5-5
...@@ -107,9 +107,9 @@ export function makeHarness(options: UpdatesHarnessOptions = {}) {...@@ -107,9 +107,9 @@ export function makeHarness(options: UpdatesHarnessOptions = {}) {
107107
108 const windowLayer = Layer.succeed(ElectronWindow.ElectronWindow, {108 const windowLayer = Layer.succeed(ElectronWindow.ElectronWindow, {
109 create: () => Effect.die("unexpected BrowserWindow creation"),109 create: () => Effect.die("unexpected BrowserWindow creation"),
110 main: Effect.succeed(Option.none()),110 main: Effect.succeedNone,
111 currentMainOrFirst: Effect.succeed(Option.none()),111 currentMainOrFirst: Effect.succeedNone,
112 focusedMainOrFirst: Effect.succeed(Option.none()),112 focusedMainOrFirst: Effect.succeedNone,
113 setMain: () => Effect.void,113 setMain: () => Effect.void,
114 clearMain: () => Effect.void,114 clearMain: () => Effect.void,
115 prepareReveal: () => Effect.succeed(false),115 prepareReveal: () => Effect.succeed(false),
...@@ -131,7 +131,7 @@ export function makeHarness(options: UpdatesHarnessOptions = {}) {...@@ -131,7 +131,7 @@ export function makeHarness(options: UpdatesHarnessOptions = {}) {
131 installSteps.push("startBackend");131 installSteps.push("startBackend");
132 }).pipe(Effect.andThen(options.startBackend ?? Effect.void)),132 }).pipe(Effect.andThen(options.startBackend ?? Effect.void)),
133 stop: () => options.stopBackend ?? Effect.void,133 stop: () => options.stopBackend ?? Effect.void,
134 currentConfig: Effect.succeed(Option.none()),134 currentConfig: Effect.succeedNone,
135 snapshot: Effect.succeed({135 snapshot: Effect.succeed({
136 desiredRunning: false,136 desiredRunning: false,
137 ready: false,137 ready: false,
...@@ -245,7 +245,7 @@ export function makeHarness(options: UpdatesHarnessOptions = {}) {...@@ -245,7 +245,7 @@ export function makeHarness(options: UpdatesHarnessOptions = {}) {
245 installSteps,245 installSteps,
246 updateRestartMarkers,246 updateRestartMarkers,
247 downloadCount: () => downloadCount,247 downloadCount: () => downloadCount,
248 feedUrls: () => feedUrls,248 feedUrls: (): ElectronUpdater.ElectronUpdaterFeedUrl[] => feedUrls,
249 fullChangelog: () => fullChangelog,249 fullChangelog: () => fullChangelog,
250 listenerCount: () =>250 listenerCount: () =>
251 Array.from(listeners.values()).reduce(251 Array.from(listeners.values()).reduce(
apps/desktop/src/window/DesktopApplicationMenu.test.ts+3-4
...@@ -3,7 +3,6 @@ import { assert, describe, it } from "@effect/vitest";...@@ -3,7 +3,6 @@ import { assert, describe, it } from "@effect/vitest";
3import * as Deferred from "effect/Deferred";3import * as Deferred from "effect/Deferred";
4import * as Effect from "effect/Effect";4import * as Effect from "effect/Effect";
5import * as Layer from "effect/Layer";5import * as Layer from "effect/Layer";
6import * as Option from "effect/Option";
76
8import type * as Electron from "electron";7import type * as Electron from "electron";
98
...@@ -51,7 +50,7 @@ const electronAppLayer = Layer.succeed(ElectronApp.ElectronApp, {...@@ -51,7 +50,7 @@ const electronAppLayer = Layer.succeed(ElectronApp.ElectronApp, {
51} satisfies ElectronApp.ElectronApp["Service"]);50} satisfies ElectronApp.ElectronApp["Service"]);
5251
53const electronDialogLayer = Layer.succeed(ElectronDialog.ElectronDialog, {52const electronDialogLayer = Layer.succeed(ElectronDialog.ElectronDialog, {
54 pickFolder: () => Effect.succeed(Option.none()),53 pickFolder: () => Effect.succeedNone,
55 pickFiles: () => Effect.succeed([]),54 pickFiles: () => Effect.succeed([]),
56 showMessageBox: () => Effect.succeed({ response: 0, checkboxChecked: false }),55 showMessageBox: () => Effect.succeed({ response: 0, checkboxChecked: false }),
57 showErrorBox: () => Effect.void,56 showErrorBox: () => Effect.void,
...@@ -63,7 +62,7 @@ const desktopUpdatesLayer = Layer.succeed(DesktopUpdates.DesktopUpdates, {...@@ -63,7 +62,7 @@ const desktopUpdatesLayer = Layer.succeed(DesktopUpdates.DesktopUpdates, {
63 isInstallActive: Effect.succeed(false),62 isInstallActive: Effect.succeed(false),
64 subscribe: Effect.die("unexpected subscribe"),63 subscribe: Effect.die("unexpected subscribe"),
65 emitState: Effect.void,64 emitState: Effect.void,
66 disabledReason: Effect.succeed(Option.none()),65 disabledReason: Effect.succeedNone,
67 configure: Effect.void,66 configure: Effect.void,
68 setChannel: () => Effect.die("unexpected setChannel"),67 setChannel: () => Effect.die("unexpected setChannel"),
69 check: () => Effect.die("unexpected check"),68 check: () => Effect.die("unexpected check"),
...@@ -98,7 +97,7 @@ const makeElectronMenuLayer = (...@@ -98,7 +97,7 @@ const makeElectronMenuLayer = (
98 setApplicationMenu: (template) =>97 setApplicationMenu: (template) =>
99 Deferred.succeed(applicationMenuTemplate, template).pipe(Effect.asVoid),98 Deferred.succeed(applicationMenuTemplate, template).pipe(Effect.asVoid),
100 popupTemplate: () => Effect.void,99 popupTemplate: () => Effect.void,
101 showContextMenu: () => Effect.succeed(Option.none()),100 showContextMenu: () => Effect.succeedNone,
102 } satisfies ElectronMenu.ElectronMenu["Service"]);101 } satisfies ElectronMenu.ElectronMenu["Service"]);
103102
104const configureMenu = (103const configureMenu = (
apps/desktop/src/window/DesktopWindow.test.ts+3-3
...@@ -151,7 +151,7 @@ function makeFakeBrowserWindow() {...@@ -151,7 +151,7 @@ function makeFakeBrowserWindow() {
151}151}
152152
153const desktopClientSettingsLayer = Layer.mock(DesktopClientSettings.DesktopClientSettings)({153const desktopClientSettingsLayer = Layer.mock(DesktopClientSettings.DesktopClientSettings)({
154 get: Effect.succeed(Option.none()),154 get: Effect.succeedNone,
155});155});
156156
157const electronAppLayer = Layer.mock(ElectronApp.ElectronApp)({157const electronAppLayer = Layer.mock(ElectronApp.ElectronApp)({
...@@ -185,7 +185,7 @@ const desktopServerExposureLayer = Layer.succeed(DesktopServerExposure.DesktopSe...@@ -185,7 +185,7 @@ const desktopServerExposureLayer = Layer.succeed(DesktopServerExposure.DesktopSe
185const electronMenuLayer = Layer.succeed(ElectronMenu.ElectronMenu, {185const electronMenuLayer = Layer.succeed(ElectronMenu.ElectronMenu, {
186 setApplicationMenu: () => Effect.void,186 setApplicationMenu: () => Effect.void,
187 popupTemplate: () => Effect.void,187 popupTemplate: () => Effect.void,
188 showContextMenu: () => Effect.succeed(Option.none()),188 showContextMenu: () => Effect.succeedNone,
189} satisfies ElectronMenu.ElectronMenu["Service"]);189} satisfies ElectronMenu.ElectronMenu["Service"]);
190190
191const electronThemeLayer = Layer.succeed(ElectronTheme.ElectronTheme, {191const electronThemeLayer = Layer.succeed(ElectronTheme.ElectronTheme, {
...@@ -294,7 +294,7 @@ function makeTestLayer(input: {...@@ -294,7 +294,7 @@ function makeTestLayer(input: {
294 electronAppLayer,294 electronAppLayer,
295 Layer.succeed(ElectronMenu.ElectronMenu, {295 Layer.succeed(ElectronMenu.ElectronMenu, {
296 setApplicationMenu: () => Effect.void,296 setApplicationMenu: () => Effect.void,
297 showContextMenu: () => Effect.succeed(Option.none()),297 showContextMenu: () => Effect.succeedNone,
298 popupTemplate: input.onPopupTemplate ?? (() => Effect.void),298 popupTemplate: input.onPopupTemplate ?? (() => Effect.void),
299 }),299 }),
300 Layer.succeed(ElectronShell.ElectronShell, {300 Layer.succeed(ElectronShell.ElectronShell, {
apps/desktop/src/wsl/DesktopWslBackend.ts+2-2
...@@ -138,7 +138,7 @@ export const layer = Layer.effect(...@@ -138,7 +138,7 @@ export const layer = Layer.effect(
138 const primaryConfig = yield* serverExposure.backendConfig;138 const primaryConfig = yield* serverExposure.backendConfig;
139 const port = yield* scanForWslPort(primaryConfig.port + 1).pipe(139 const port = yield* scanForWslPort(primaryConfig.port + 1).pipe(
140 Effect.provideService(NetService.NetService, net),140 Effect.provideService(NetService.NetService, net),
141 Effect.map((value) => Option.some(value)),141 Effect.asSome,
142 Effect.catch((error) =>142 Effect.catch((error) =>
143 logWslBackendWarning("could not allocate port for WSL backend", {143 logWslBackendWarning("could not allocate port for WSL backend", {
144 error: error.message,144 error: error.message,
...@@ -171,7 +171,7 @@ export const layer = Layer.effect(...@@ -171,7 +171,7 @@ export const layer = Layer.effect(
171 onReady: () => Ref.set(preflightErrorRef, Option.none()),171 onReady: () => Ref.set(preflightErrorRef, Option.none()),
172 })172 })
173 .pipe(173 .pipe(
174 Effect.map((registered) => Option.some(registered)),174 Effect.asSome,
175 Effect.catch((error) =>175 Effect.catch((error) =>
176 logWslBackendWarning("WSL backend already registered, skipping start", {176 logWslBackendWarning("WSL backend already registered, skipping start", {
177 id: targetId,177 id: targetId,
apps/desktop/src/wsl/DesktopWslEnvironment.ts+9-14
...@@ -1040,11 +1040,7 @@ const preWarmImpl = (...@@ -1040,11 +1040,7 @@ const preWarmImpl = (
1040 const handle = yield* spawner.spawn(command);1040 const handle = yield* spawner.spawn(command);
1041 yield* handle.exitCode;1041 yield* handle.exitCode;
1042 }),1042 }),
1043 ).pipe(1043 ).pipe(Effect.timeoutOption(PRE_WARM_TIMEOUT), Effect.ignore);
1044 Effect.timeoutOption(PRE_WARM_TIMEOUT),
1045 Effect.asVoid,
1046 Effect.catch(() => Effect.void),
1047 );
10481044
1049const windowsToWslPathImpl = (1045const windowsToWslPathImpl = (
1050 distro: string | null,1046 distro: string | null,
...@@ -1272,15 +1268,14 @@ export const layer = Layer.effect(...@@ -1272,15 +1268,14 @@ export const layer = Layer.effect(
1272 // distro. Negative results aren't cached so a transient wsl.exe failure1268 // distro. Negative results aren't cached so a transient wsl.exe failure
1273 // doesn't permanently disable tilde expansion.1269 // doesn't permanently disable tilde expansion.
1274 const userHomeCache = new Map<string, string>();1270 const userHomeCache = new Map<string, string>();
1275 const getUserHome = (distro: string | null) =>1271 const getUserHome = Effect.fn("desktop.wsl.getUserHome")(function* (distro: string | null) {
1276 Effect.gen(function* () {1272 const key = distro ?? "__default__";
1277 const key = distro ?? "__default__";1273 const cached = userHomeCache.get(key);
1278 const cached = userHomeCache.get(key);1274 if (cached !== undefined) return Option.some(cached);
1279 if (cached !== undefined) return Option.some(cached);1275 const resolved = yield* provideSpawner(getUserHomeImpl(distro));
1280 const resolved = yield* provideSpawner(getUserHomeImpl(distro));1276 if (Option.isSome(resolved)) userHomeCache.set(key, resolved.value);
1281 if (Option.isSome(resolved)) userHomeCache.set(key, resolved.value);1277 return resolved;
1282 return resolved;1278 });
1283 }).pipe(Effect.withSpan("desktop.wsl.getUserHome"));
12841279
1285 const getDistroIp = (distro: string | null) =>1280 const getDistroIp = (distro: string | null) =>
1286 provideSpawner(getDistroIpImpl(distro)).pipe(Effect.withSpan("desktop.wsl.getDistroIp"));1281 provideSpawner(getDistroIpImpl(distro)).pipe(Effect.withSpan("desktop.wsl.getDistroIp"));
apps/server/integration/orphanedProviderSessionStartup.integration.test.ts+1-1
...@@ -100,7 +100,7 @@ const startupDependencies = Layer.mergeAll(...@@ -100,7 +100,7 @@ const startupDependencies = Layer.mergeAll(
100 Layer.succeed(ServiceLauncherClient.ServiceLauncherClient, {100 Layer.succeed(ServiceLauncherClient.ServiceLauncherClient, {
101 managed: false,101 managed: false,
102 requestUpdate: () => Effect.die("unused"),102 requestUpdate: () => Effect.die("unused"),
103 prepareTrial: Effect.sync(() => undefined),103 prepareTrial: Effect.undefined,
104 }),104 }),
105 Layer.succeed(105 Layer.succeed(
106 HttpServer.HttpServer,106 HttpServer.HttpServer,
apps/server/src/assets/AssetAccess.ts+3-3
...@@ -190,7 +190,7 @@ const optionOnNotFound = <A, R>(...@@ -190,7 +190,7 @@ const optionOnNotFound = <A, R>(
190 effect: Effect.Effect<A, PlatformError.PlatformError, R>,190 effect: Effect.Effect<A, PlatformError.PlatformError, R>,
191): Effect.Effect<Option.Option<A>, PlatformError.PlatformError, R> =>191): Effect.Effect<Option.Option<A>, PlatformError.PlatformError, R> =>
192 effect.pipe(192 effect.pipe(
193 Effect.map(Option.some),193 Effect.asSome,
194 Effect.catchTags({194 Effect.catchTags({
195 PlatformError: (error) =>195 PlatformError: (error) =>
196 error.reason._tag === "NotFound" ? Effect.succeed(Option.none<A>()) : Effect.fail(error),196 error.reason._tag === "NotFound" ? Effect.succeed(Option.none<A>()) : Effect.fail(error),
...@@ -213,9 +213,9 @@ const resolveCanonicalWorkspaceFile = Effect.fn("AssetAccess.resolveCanonicalWor...@@ -213,9 +213,9 @@ const resolveCanonicalWorkspaceFile = Effect.fn("AssetAccess.resolveCanonicalWor
213 const fileSystem = yield* FileSystem.FileSystem;213 const fileSystem = yield* FileSystem.FileSystem;
214 const workspacePaths = yield* WorkspacePaths.WorkspacePaths;214 const workspacePaths = yield* WorkspacePaths.WorkspacePaths;
215 const resolved = yield* workspacePaths.resolveRelativePathWithinRoot(input).pipe(215 const resolved = yield* workspacePaths.resolveRelativePathWithinRoot(input).pipe(
216 Effect.map(Option.some),216 Effect.asSome,
217 Effect.catchTags({217 Effect.catchTags({
218 WorkspacePathOutsideRootError: () => Effect.succeed(Option.none()),218 WorkspacePathOutsideRootError: () => Effect.succeedNone,
219 }),219 }),
220 );220 );
221 if (Option.isNone(resolved)) return null;221 if (Option.isNone(resolved)) return null;
apps/server/src/assets/NativeAppIconResolver.ts+2-2
...@@ -43,10 +43,10 @@ function appFromCacheKey(key: string): ToolActivityNativeAppReference {...@@ -43,10 +43,10 @@ function appFromCacheKey(key: string): ToolActivityNativeAppReference {
43const existingFile = Effect.fn("NativeAppIconResolver.existingFile")(function* (filePath: string) {43const existingFile = Effect.fn("NativeAppIconResolver.existingFile")(function* (filePath: string) {
44 const fileSystem = yield* FileSystem.FileSystem;44 const fileSystem = yield* FileSystem.FileSystem;
45 const info = yield* fileSystem.stat(filePath).pipe(45 const info = yield* fileSystem.stat(filePath).pipe(
46 Effect.map(Option.some),46 Effect.asSome,
47 Effect.catchTags({47 Effect.catchTags({
48 PlatformError: (error) =>48 PlatformError: (error) =>
49 error.reason._tag === "NotFound" ? Effect.succeed(Option.none()) : Effect.fail(error),49 error.reason._tag === "NotFound" ? Effect.succeedNone : Effect.fail(error),
50 }),50 }),
51 );51 );
52 return Option.isSome(info) && info.value.type === "File" ? filePath : null;52 return Option.isSome(info) && info.value.type === "File" ? filePath : null;
apps/server/src/auth/PairingGrantStore.test.ts+2-3
...@@ -3,7 +3,6 @@ import { expect, it } from "@effect/vitest";...@@ -3,7 +3,6 @@ import { expect, it } from "@effect/vitest";
3import * as Duration from "effect/Duration";3import * as Duration from "effect/Duration";
4import * as Effect from "effect/Effect";4import * as Effect from "effect/Effect";
5import * as Layer from "effect/Layer";5import * as Layer from "effect/Layer";
6import * as Option from "effect/Option";
7import * as Stream from "effect/Stream";6import * as Stream from "effect/Stream";
8import * as Queue from "effect/Queue";7import * as Queue from "effect/Queue";
9import * as TestClock from "effect/testing/TestClock";8import * as TestClock from "effect/testing/TestClock";
...@@ -47,10 +46,10 @@ const makePairingGrantStoreTestLayer = (...@@ -47,10 +46,10 @@ const makePairingGrantStoreTestLayer = (
47 AuthPairingLinks.AuthPairingLinkRepository,46 AuthPairingLinks.AuthPairingLinkRepository,
48 AuthPairingLinks.AuthPairingLinkRepository.of({47 AuthPairingLinks.AuthPairingLinkRepository.of({
49 create: () => Effect.void,48 create: () => Effect.void,
50 consumeAvailable: () => Effect.succeed(Option.none()),49 consumeAvailable: () => Effect.succeedNone,
51 listActive: () => Effect.succeed([]),50 listActive: () => Effect.succeed([]),
52 revoke: () => Effect.succeed(false),51 revoke: () => Effect.succeed(false),
53 getByCredential: () => Effect.succeed(Option.none()),52 getByCredential: () => Effect.succeedNone,
54 ...overrides,53 ...overrides,
55 }),54 }),
56 ),55 ),
apps/server/src/auth/ServerSecretStore.ts+1-1
...@@ -174,7 +174,7 @@ export const make = Effect.gen(function* () {...@@ -174,7 +174,7 @@ export const make = Effect.gen(function* () {
174 Effect.map((bytes) => Option.some(Uint8Array.from(bytes))),174 Effect.map((bytes) => Option.some(Uint8Array.from(bytes))),
175 Effect.catch((cause) =>175 Effect.catch((cause) =>
176 cause.reason._tag === "NotFound"176 cause.reason._tag === "NotFound"
177 ? Effect.succeed(Option.none())177 ? Effect.succeedNone
178 : Effect.fail(178 : Effect.fail(
179 new SecretStoreReadError({179 new SecretStoreReadError({
180 resource: `secret ${name}`,180 resource: `secret ${name}`,
apps/server/src/auth/dpop.ts+1-1
...@@ -115,7 +115,7 @@ export const verifyRequestDpopProof = (input: {...@@ -115,7 +115,7 @@ export const verifyRequestDpopProof = (input: {
115 "environment.dpop.failure_code": mapped.dpopFailureReason,115 "environment.dpop.failure_code": mapped.dpopFailureReason,
116 });116 });
117 }117 }
118 return yield* Effect.fail(mapped);118 return yield* mapped;
119 }),119 }),
120 ),120 ),
121 );121 );
apps/server/src/auth/http.test.ts+1-2
...@@ -5,7 +5,6 @@ import * as Context from "effect/Context";...@@ -5,7 +5,6 @@ import * as Context from "effect/Context";
5import * as Crypto from "effect/Crypto";5import * as Crypto from "effect/Crypto";
6import * as Effect from "effect/Effect";6import * as Effect from "effect/Effect";
7import * as Layer from "effect/Layer";7import * as Layer from "effect/Layer";
8import * as Option from "effect/Option";
9import * as Redacted from "effect/Redacted";8import * as Redacted from "effect/Redacted";
10import * as Schema from "effect/Schema";9import * as Schema from "effect/Schema";
11import * as Etag from "effect/unstable/http/Etag";10import * as Etag from "effect/unstable/http/Etag";
...@@ -69,7 +68,7 @@ it.effect("sets the selected browser session cookies through the HTTP route", ()...@@ -69,7 +68,7 @@ it.effect("sets the selected browser session cookies through the HTTP route", ()
69 Effect.gen(function* () {68 Effect.gen(function* () {
70 const crypto = yield* Crypto.Crypto;69 const crypto = yield* Crypto.Crypto;
71 const unusedSecretStore = ServerSecretStore.ServerSecretStore.of({70 const unusedSecretStore = ServerSecretStore.ServerSecretStore.of({
72 get: () => Effect.succeed(Option.none()),71 get: () => Effect.succeedNone,
73 set: () => Effect.void,72 set: () => Effect.void,
74 create: () => Effect.void,73 create: () => Effect.void,
75 getOrCreateRandom: () => Effect.die("Not used by these routes."),74 getOrCreateRandom: () => Effect.die("Not used by these routes."),
apps/server/src/checkpointing/CheckpointDiffQuery.test.ts+35-35
...@@ -89,10 +89,10 @@ describe("CheckpointDiffQuery.layer", () => {...@@ -89,10 +89,10 @@ describe("CheckpointDiffQuery.layer", () => {
89 getSnapshotSequence: () => Effect.succeed({ snapshotSequence: 0 }),89 getSnapshotSequence: () => Effect.succeed({ snapshotSequence: 0 }),
90 getCounts: () => Effect.succeed({ projectCount: 0, threadCount: 0 }),90 getCounts: () => Effect.succeed({ projectCount: 0, threadCount: 0 }),
91 getEventReplayStats: () => Effect.die("unused"),91 getEventReplayStats: () => Effect.die("unused"),
92 getActiveProjectByWorkspaceRoot: () => Effect.succeed(Option.none()),92 getActiveProjectByWorkspaceRoot: () => Effect.succeedNone,
93 getProjectShells: () => Effect.die("unused"),93 getProjectShells: () => Effect.die("unused"),
94 getProjectShellById: () => Effect.succeed(Option.none()),94 getProjectShellById: () => Effect.succeedNone,
95 getFirstActiveThreadIdByProjectId: () => Effect.succeed(Option.none()),95 getFirstActiveThreadIdByProjectId: () => Effect.succeedNone,
96 getImportedAgentSessionSources: () => Effect.die("unused"),96 getImportedAgentSessionSources: () => Effect.die("unused"),
97 getThreadCheckpointContext: () =>97 getThreadCheckpointContext: () =>
98 Effect.sync(() => {98 Effect.sync(() => {
...@@ -113,9 +113,9 @@ describe("CheckpointDiffQuery.layer", () => {...@@ -113,9 +113,9 @@ describe("CheckpointDiffQuery.layer", () => {
113 }),113 }),
114 getThreadRuntimeContext: () => Effect.die("unused"),114 getThreadRuntimeContext: () => Effect.die("unused"),
115 getTurnStartMessage: () => Effect.die("unused"),115 getTurnStartMessage: () => Effect.die("unused"),
116 getThreadShellById: () => Effect.succeed(Option.none()),116 getThreadShellById: () => Effect.succeedNone,
117 getThreadDetailById: () => Effect.succeed(Option.none()),117 getThreadDetailById: () => Effect.succeedNone,
118 getThreadDetailSnapshot: () => Effect.succeed(Option.none()),118 getThreadDetailSnapshot: () => Effect.succeedNone,
119 searchThreads: () => Effect.succeed({ matches: [] }),119 searchThreads: () => Effect.succeed({ matches: [] }),
120 }),120 }),
121 ),121 ),
...@@ -206,18 +206,18 @@ describe("CheckpointDiffQuery.layer", () => {...@@ -206,18 +206,18 @@ describe("CheckpointDiffQuery.layer", () => {
206 getSnapshotSequence: () => Effect.succeed({ snapshotSequence: 0 }),206 getSnapshotSequence: () => Effect.succeed({ snapshotSequence: 0 }),
207 getCounts: () => Effect.succeed({ projectCount: 0, threadCount: 0 }),207 getCounts: () => Effect.succeed({ projectCount: 0, threadCount: 0 }),
208 getEventReplayStats: () => Effect.die("unused"),208 getEventReplayStats: () => Effect.die("unused"),
209 getActiveProjectByWorkspaceRoot: () => Effect.succeed(Option.none()),209 getActiveProjectByWorkspaceRoot: () => Effect.succeedNone,
210 getProjectShells: () => Effect.die("unused"),210 getProjectShells: () => Effect.die("unused"),
211 getProjectShellById: () => Effect.succeed(Option.none()),211 getProjectShellById: () => Effect.succeedNone,
212 getFirstActiveThreadIdByProjectId: () => Effect.succeed(Option.none()),212 getFirstActiveThreadIdByProjectId: () => Effect.succeedNone,
213 getImportedAgentSessionSources: () => Effect.die("unused"),213 getImportedAgentSessionSources: () => Effect.die("unused"),
214 getThreadCheckpointContext: () => Effect.succeed(Option.some(threadCheckpointContext)),214 getThreadCheckpointContext: () => Effect.succeedSome(threadCheckpointContext),
215 getFullThreadDiffContext: () => Effect.die("unused"),215 getFullThreadDiffContext: () => Effect.die("unused"),
216 getThreadRuntimeContext: () => Effect.die("unused"),216 getThreadRuntimeContext: () => Effect.die("unused"),
217 getTurnStartMessage: () => Effect.die("unused"),217 getTurnStartMessage: () => Effect.die("unused"),
218 getThreadShellById: () => Effect.succeed(Option.none()),218 getThreadShellById: () => Effect.succeedNone,
219 getThreadDetailById: () => Effect.succeed(Option.none()),219 getThreadDetailById: () => Effect.succeedNone,
220 getThreadDetailSnapshot: () => Effect.succeed(Option.none()),220 getThreadDetailSnapshot: () => Effect.succeedNone,
221 searchThreads: () => Effect.succeed({ matches: [] }),221 searchThreads: () => Effect.succeed({ matches: [] }),
222 }),222 }),
223 ),223 ),
...@@ -298,18 +298,18 @@ describe("CheckpointDiffQuery.layer", () => {...@@ -298,18 +298,18 @@ describe("CheckpointDiffQuery.layer", () => {
298 getSnapshotSequence: () => Effect.succeed({ snapshotSequence: 0 }),298 getSnapshotSequence: () => Effect.succeed({ snapshotSequence: 0 }),
299 getCounts: () => Effect.succeed({ projectCount: 0, threadCount: 0 }),299 getCounts: () => Effect.succeed({ projectCount: 0, threadCount: 0 }),
300 getEventReplayStats: () => Effect.die("unused"),300 getEventReplayStats: () => Effect.die("unused"),
301 getActiveProjectByWorkspaceRoot: () => Effect.succeed(Option.none()),301 getActiveProjectByWorkspaceRoot: () => Effect.succeedNone,
302 getProjectShells: () => Effect.die("unused"),302 getProjectShells: () => Effect.die("unused"),
303 getProjectShellById: () => Effect.succeed(Option.none()),303 getProjectShellById: () => Effect.succeedNone,
304 getFirstActiveThreadIdByProjectId: () => Effect.succeed(Option.none()),304 getFirstActiveThreadIdByProjectId: () => Effect.succeedNone,
305 getImportedAgentSessionSources: () => Effect.die("unused"),305 getImportedAgentSessionSources: () => Effect.die("unused"),
306 getThreadCheckpointContext: () => Effect.succeed(Option.some(threadCheckpointContext)),306 getThreadCheckpointContext: () => Effect.succeedSome(threadCheckpointContext),
307 getFullThreadDiffContext: () => Effect.die("unused"),307 getFullThreadDiffContext: () => Effect.die("unused"),
308 getThreadRuntimeContext: () => Effect.die("unused"),308 getThreadRuntimeContext: () => Effect.die("unused"),
309 getTurnStartMessage: () => Effect.die("unused"),309 getTurnStartMessage: () => Effect.die("unused"),
310 getThreadShellById: () => Effect.succeed(Option.none()),310 getThreadShellById: () => Effect.succeedNone,
311 getThreadDetailById: () => Effect.succeed(Option.none()),311 getThreadDetailById: () => Effect.succeedNone,
312 getThreadDetailSnapshot: () => Effect.succeed(Option.none()),312 getThreadDetailSnapshot: () => Effect.succeedNone,
313 searchThreads: () => Effect.succeed({ matches: [] }),313 searchThreads: () => Effect.succeed({ matches: [] }),
314 }),314 }),
315 ),315 ),
...@@ -375,18 +375,18 @@ describe("CheckpointDiffQuery.layer", () => {...@@ -375,18 +375,18 @@ describe("CheckpointDiffQuery.layer", () => {
375 getSnapshotSequence: () => Effect.succeed({ snapshotSequence: 0 }),375 getSnapshotSequence: () => Effect.succeed({ snapshotSequence: 0 }),
376 getCounts: () => Effect.succeed({ projectCount: 0, threadCount: 0 }),376 getCounts: () => Effect.succeed({ projectCount: 0, threadCount: 0 }),
377 getEventReplayStats: () => Effect.die("unused"),377 getEventReplayStats: () => Effect.die("unused"),
378 getActiveProjectByWorkspaceRoot: () => Effect.succeed(Option.none()),378 getActiveProjectByWorkspaceRoot: () => Effect.succeedNone,
379 getProjectShells: () => Effect.die("unused"),379 getProjectShells: () => Effect.die("unused"),
380 getProjectShellById: () => Effect.succeed(Option.none()),380 getProjectShellById: () => Effect.succeedNone,
381 getFirstActiveThreadIdByProjectId: () => Effect.succeed(Option.none()),381 getFirstActiveThreadIdByProjectId: () => Effect.succeedNone,
382 getImportedAgentSessionSources: () => Effect.die("unused"),382 getImportedAgentSessionSources: () => Effect.die("unused"),
383 getThreadCheckpointContext: () => Effect.succeed(Option.some(threadCheckpointContext)),383 getThreadCheckpointContext: () => Effect.succeedSome(threadCheckpointContext),
384 getFullThreadDiffContext: () => Effect.die("unused"),384 getFullThreadDiffContext: () => Effect.die("unused"),
385 getThreadRuntimeContext: () => Effect.die("unused"),385 getThreadRuntimeContext: () => Effect.die("unused"),
386 getTurnStartMessage: () => Effect.die("unused"),386 getTurnStartMessage: () => Effect.die("unused"),
387 getThreadShellById: () => Effect.succeed(Option.none()),387 getThreadShellById: () => Effect.succeedNone,
388 getThreadDetailById: () => Effect.succeed(Option.none()),388 getThreadDetailById: () => Effect.succeedNone,
389 getThreadDetailSnapshot: () => Effect.succeed(Option.none()),389 getThreadDetailSnapshot: () => Effect.succeedNone,
390 searchThreads: () => Effect.succeed({ matches: [] }),390 searchThreads: () => Effect.succeed({ matches: [] }),
391 }),391 }),
392 ),392 ),
...@@ -437,18 +437,18 @@ describe("CheckpointDiffQuery.layer", () => {...@@ -437,18 +437,18 @@ describe("CheckpointDiffQuery.layer", () => {
437 getSnapshotSequence: () => Effect.succeed({ snapshotSequence: 0 }),437 getSnapshotSequence: () => Effect.succeed({ snapshotSequence: 0 }),
438 getCounts: () => Effect.succeed({ projectCount: 0, threadCount: 0 }),438 getCounts: () => Effect.succeed({ projectCount: 0, threadCount: 0 }),
439 getEventReplayStats: () => Effect.die("unused"),439 getEventReplayStats: () => Effect.die("unused"),
440 getActiveProjectByWorkspaceRoot: () => Effect.succeed(Option.none()),440 getActiveProjectByWorkspaceRoot: () => Effect.succeedNone,
441 getProjectShells: () => Effect.die("unused"),441 getProjectShells: () => Effect.die("unused"),
442 getProjectShellById: () => Effect.succeed(Option.none()),442 getProjectShellById: () => Effect.succeedNone,
443 getFirstActiveThreadIdByProjectId: () => Effect.succeed(Option.none()),443 getFirstActiveThreadIdByProjectId: () => Effect.succeedNone,
444 getImportedAgentSessionSources: () => Effect.die("unused"),444 getImportedAgentSessionSources: () => Effect.die("unused"),
445 getThreadCheckpointContext: () => Effect.succeed(Option.none()),445 getThreadCheckpointContext: () => Effect.succeedNone,
446 getFullThreadDiffContext: () => Effect.succeed(Option.none()),446 getFullThreadDiffContext: () => Effect.succeedNone,
447 getThreadRuntimeContext: () => Effect.die("unused"),447 getThreadRuntimeContext: () => Effect.die("unused"),
448 getTurnStartMessage: () => Effect.die("unused"),448 getTurnStartMessage: () => Effect.die("unused"),
449 getThreadShellById: () => Effect.succeed(Option.none()),449 getThreadShellById: () => Effect.succeedNone,
450 getThreadDetailById: () => Effect.succeed(Option.none()),450 getThreadDetailById: () => Effect.succeedNone,
451 getThreadDetailSnapshot: () => Effect.succeed(Option.none()),451 getThreadDetailSnapshot: () => Effect.succeedNone,
452 searchThreads: () => Effect.succeed({ matches: [] }),452 searchThreads: () => Effect.succeed({ matches: [] }),
453 }),453 }),
454 ),454 ),
apps/server/src/cli/theme.ts+17-20
...@@ -260,12 +260,10 @@ const writeDefaultTheme = Effect.fn(function* (input: {...@@ -260,12 +260,10 @@ const writeDefaultTheme = Effect.fn(function* (input: {
260 // Falling through here would overwrite whatever landed in between, which260 // Falling through here would overwrite whatever landed in between, which
261 // is exactly the loss this loop exists to prevent.261 // is exactly the loss this loop exists to prevent.
262 if (attempt >= CONCURRENT_WRITE_ATTEMPTS) {262 if (attempt >= CONCURRENT_WRITE_ATTEMPTS) {
263 return yield* Effect.fail(263 return yield* new ThemeSettingsBusyError({
264 new ThemeSettingsBusyError({264 settingsPath: input.settingsPath,
265 settingsPath: input.settingsPath,265 attempts: CONCURRENT_WRITE_ATTEMPTS,
266 attempts: CONCURRENT_WRITE_ATTEMPTS,266 });
267 }),
268 );
269 }267 }
270 continue;268 continue;
271 }269 }
...@@ -299,12 +297,13 @@ const publishThemeFile = Effect.fn(function* (input: {...@@ -299,12 +297,13 @@ const publishThemeFile = Effect.fn(function* (input: {
299 Effect.mapError((cause) => new ThemeFileUnreadableError({ filePath: input.filePath, cause })),297 Effect.mapError((cause) => new ThemeFileUnreadableError({ filePath: input.filePath, cause })),
300 );298 );
301 if (info.type !== "File") {299 if (info.type !== "File") {
302 return yield* Effect.fail(new ThemeFileUnreadableError({ filePath: input.filePath }));300 return yield* new ThemeFileUnreadableError({ filePath: input.filePath });
303 }301 }
304 if (Number(info.size) > MAX_THEME_FILE_BYTES) {302 if (Number(info.size) > MAX_THEME_FILE_BYTES) {
305 return yield* Effect.fail(303 return yield* new ThemeFileTooLargeError({
306 new ThemeFileTooLargeError({ filePath: input.filePath, limit: MAX_THEME_FILE_BYTES }),304 filePath: input.filePath,
307 );305 limit: MAX_THEME_FILE_BYTES,
306 });
308 }307 }
309308
310 // An explicit source path is the user's own input, and a symlink there is a309 // An explicit source path is the user's own input, and a symlink there is a
...@@ -320,17 +319,15 @@ const publishThemeFile = Effect.fn(function* (input: {...@@ -320,17 +319,15 @@ const publishThemeFile = Effect.fn(function* (input: {
320 );319 );
321 const raw = readThemeFileGuarded(resolvedSource, MAX_THEME_FILE_BYTES);320 const raw = readThemeFileGuarded(resolvedSource, MAX_THEME_FILE_BYTES);
322 if (raw === null) {321 if (raw === null) {
323 return yield* Effect.fail(new ThemeFileUnreadableError({ filePath: input.filePath }));322 return yield* new ThemeFileUnreadableError({ filePath: input.filePath });
324 }323 }
325324
326 const decoded = decodeThemeFileJsonExit(raw);325 const decoded = decodeThemeFileJsonExit(raw);
327 if (decoded._tag === "Failure") {326 if (decoded._tag === "Failure") {
328 return yield* Effect.fail(327 return yield* new ThemeFileInvalidError({ filePath: input.filePath, cause: decoded.cause });
329 new ThemeFileInvalidError({ filePath: input.filePath, cause: decoded.cause }),
330 );
331 }328 }
332 if (!environmentThemeFileHasColors(decoded.value)) {329 if (!environmentThemeFileHasColors(decoded.value)) {
333 return yield* Effect.fail(new ThemeFileColorlessError({ filePath: input.filePath }));330 return yield* new ThemeFileColorlessError({ filePath: input.filePath });
334 }331 }
335332
336 const fileBasename = path.basename(input.filePath, ".json");333 const fileBasename = path.basename(input.filePath, ".json");
...@@ -338,7 +335,7 @@ const publishThemeFile = Effect.fn(function* (input: {...@@ -338,7 +335,7 @@ const publishThemeFile = Effect.fn(function* (input: {
338 // The same rules the watcher applies when it reads the directory back, so a335 // The same rules the watcher applies when it reads the directory back, so a
339 // publish cannot report success for a file that will then be skipped.336 // publish cannot report success for a file that will then be skipped.
340 if (!isEnvironmentThemeId(themeId) || UNPUBLISHABLE_THEME_IDS.has(themeId)) {337 if (!isEnvironmentThemeId(themeId) || UNPUBLISHABLE_THEME_IDS.has(themeId)) {
341 return yield* Effect.fail(new ThemeFileIdInvalidError({ themeId, filePath: input.filePath }));338 return yield* new ThemeFileIdInvalidError({ themeId, filePath: input.filePath });
342 }339 }
343340
344 const destinationPath = path.join(input.themesDir, `${themeId}.json`);341 const destinationPath = path.join(input.themesDir, `${themeId}.json`);
...@@ -479,7 +476,7 @@ const themeSetCommand = Command.make("set", {...@@ -479,7 +476,7 @@ const themeSetCommand = Command.make("set", {
479 const fs = yield* FileSystem.FileSystem;476 const fs = yield* FileSystem.FileSystem;
480 const target = yield* expandHomePath(flags.theme.trim());477 const target = yield* expandHomePath(flags.theme.trim());
481 if (target.length === 0) {478 if (target.length === 0) {
482 return yield* Effect.fail(new ThemeTargetMissingError());479 return yield* new ThemeTargetMissingError();
483 }480 }
484 const paths = yield* resolveThemePaths(flags.baseDir);481 const paths = yield* resolveThemePaths(flags.baseDir);
485482
...@@ -514,15 +511,15 @@ const themeSetCommand = Command.make("set", {...@@ -514,15 +511,15 @@ const themeSetCommand = Command.make("set", {
514 revertPublish = published.revert;511 revertPublish = published.revert;
515 cleanupPublish = published.cleanup;512 cleanupPublish = published.cleanup;
516 } else if (looksLikePath) {513 } else if (looksLikePath) {
517 return yield* Effect.fail(new ThemeFileUnreadableError({ filePath: target }));514 return yield* new ThemeFileUnreadableError({ filePath: target });
518 } else if (isEnvironmentThemeId(target)) {515 } else if (isEnvironmentThemeId(target)) {
519 const known = yield* resolvableThemeIds(paths.themesDir);516 const known = yield* resolvableThemeIds(paths.themesDir);
520 if (!known.includes(target)) {517 if (!known.includes(target)) {
521 return yield* Effect.fail(new ThemeIdUnknownError({ themeId: target, known }));518 return yield* new ThemeIdUnknownError({ themeId: target, known });
522 }519 }
523 themeId = target;520 themeId = target;
524 } else {521 } else {
525 return yield* Effect.fail(new ThemeIdInvalidError({ themeId: target }));522 return yield* new ThemeIdInvalidError({ themeId: target });
526 }523 }
527524
528 // set means set: if the default cannot be written, the publish that525 // set means set: if the default cannot be written, the publish that
apps/server/src/cloud/CliTokenManager.ts+1-1
...@@ -341,7 +341,7 @@ const pollDeviceToken = Effect.fn("cloud.cli_token.poll_device_token")(function*...@@ -341,7 +341,7 @@ const pollDeviceToken = Effect.fn("cloud.cli_token.poll_device_token")(function*
341 const response = yield* HttpClientRequest.post(metadata.tokenEndpoint).pipe(341 const response = yield* HttpClientRequest.post(metadata.tokenEndpoint).pipe(
342 HttpClientRequest.bodyUrlParams(params),342 HttpClientRequest.bodyUrlParams(params),
343 httpClient.execute,343 httpClient.execute,
344 Effect.map(Option.some),344 Effect.asSome,
345 Effect.catchIf(isTransportError, () => Effect.succeedNone),345 Effect.catchIf(isTransportError, () => Effect.succeedNone),
346 );346 );
347 // Transport failures and upstream 5xx are transient while the device code347 // Transport failures and upstream 5xx are transient while the device code
apps/server/src/cloud/ManagedEndpointRuntime.test.ts+1-1
...@@ -40,7 +40,7 @@ const runtimeDependencies = (...@@ -40,7 +40,7 @@ const runtimeDependencies = (
40 Layer.succeed(ChildProcessSpawner.ChildProcessSpawner, spawner),40 Layer.succeed(ChildProcessSpawner.ChildProcessSpawner, spawner),
41 relayClientLayer,41 relayClientLayer,
42 Layer.mock(ServerSecretStore.ServerSecretStore)({42 Layer.mock(ServerSecretStore.ServerSecretStore)({
43 get: () => Effect.succeed(Option.none()),43 get: () => Effect.succeedNone,
44 }),44 }),
45 );45 );
4646
apps/server/src/cloud/http.test.ts+2-2
...@@ -252,7 +252,7 @@ describe("reconcileDesiredCloudLink", () => {...@@ -252,7 +252,7 @@ describe("reconcileDesiredCloudLink", () => {
252 CliTokenManager.CloudCliTokenManager,252 CliTokenManager.CloudCliTokenManager,
253 CliTokenManager.CloudCliTokenManager.of({253 CliTokenManager.CloudCliTokenManager.of({
254 get: unusedSecretStoreOperation(),254 get: unusedSecretStoreOperation(),
255 getExisting: Effect.succeed(Option.none()),255 getExisting: Effect.succeedNone,
256 hasCredential: unusedSecretStoreOperation(),256 hasCredential: unusedSecretStoreOperation(),
257 store: () => unusedSecretStoreOperation(),257 store: () => unusedSecretStoreOperation(),
258 clear: unusedSecretStoreOperation(),258 clear: unusedSecretStoreOperation(),
...@@ -411,7 +411,7 @@ describe("releaseManagedTunnelOnShutdown", () => {...@@ -411,7 +411,7 @@ describe("releaseManagedTunnelOnShutdown", () => {
411 CliTokenManager.CloudCliTokenManager,411 CliTokenManager.CloudCliTokenManager,
412 CliTokenManager.CloudCliTokenManager.of({412 CliTokenManager.CloudCliTokenManager.of({
413 get: unusedSecretStoreOperation(),413 get: unusedSecretStoreOperation(),
414 getExisting: Effect.succeed(Option.some(cliToken)),414 getExisting: Effect.succeedSome(cliToken),
415 hasCredential: unusedSecretStoreOperation(),415 hasCredential: unusedSecretStoreOperation(),
416 store: () => unusedSecretStoreOperation(),416 store: () => unusedSecretStoreOperation(),
417 clear: unusedSecretStoreOperation(),417 clear: unusedSecretStoreOperation(),
apps/server/src/cloud/http.ts+3-3
...@@ -152,8 +152,9 @@ export function consumeCloudReplayGuards(input: {...@@ -152,8 +152,9 @@ export function consumeCloudReplayGuards(input: {
152 readonly names: ReadonlyArray<string>;152 readonly names: ReadonlyArray<string>;
153 readonly value: Uint8Array;153 readonly value: Uint8Array;
154}) {154}) {
155 return Effect.all(155 return Effect.forEach(
156 input.names.map((name) =>156 input.names,
157 (name) =>
157 input.secrets.create(name, input.value).pipe(158 input.secrets.create(name, input.value).pipe(
158 Effect.as(true),159 Effect.as(true),
159 Effect.catchIf(ServerSecretStore.isSecretStoreError, (error) =>160 Effect.catchIf(ServerSecretStore.isSecretStoreError, (error) =>
...@@ -162,7 +163,6 @@ export function consumeCloudReplayGuards(input: {...@@ -162,7 +163,6 @@ export function consumeCloudReplayGuards(input: {
162 : Effect.fail(error),163 : Effect.fail(error),
163 ),164 ),
164 ),165 ),
165 ),
166 { concurrency: input.names.length },166 { concurrency: input.names.length },
167 ).pipe(Effect.map((created) => created.every(Boolean)));167 ).pipe(Effect.map((created) => created.every(Boolean)));
168}168}
apps/server/src/cloud/managedTunnelStartup.ts+8-7
...@@ -49,19 +49,20 @@ export const retryManagedTunnelRegistration = <A, E, R>(...@@ -49,19 +49,20 @@ export const retryManagedTunnelRegistration = <A, E, R>(
49 ),49 ),
50 Schedule.jittered,50 Schedule.jittered,
51 );51 );
52 return registration.pipe(52 const withinWindow = registration.pipe(
53 Effect.retry({53 Effect.retry({
54 while: isRetryable,54 while: isRetryable,
55 schedule: schedule.pipe(55 schedule: schedule.pipe(
56 Schedule.upTo({ duration: MANAGED_TUNNEL_REGISTRATION_RETRY_WINDOW }),56 Schedule.upTo({ duration: MANAGED_TUNNEL_REGISTRATION_RETRY_WINDOW }),
57 ),57 ),
58 }),58 }),
59 Effect.catch((error) =>59 );
60 onRetryWindowExhausted !== undefined && isRetryable(error)60 if (onRetryWindowExhausted === undefined) return withinWindow;
61 ? onRetryWindowExhausted.pipe(61 return withinWindow.pipe(
62 Effect.andThen(registration.pipe(Effect.retry({ while: isRetryable, schedule }))),62 Effect.catchIf(isRetryable, () =>
63 )63 onRetryWindowExhausted.pipe(
64 : Effect.fail(error),64 Effect.andThen(registration.pipe(Effect.retry({ while: isRetryable, schedule }))),
65 ),
65 ),66 ),
66 );67 );
67};68};
apps/server/src/cloud/selfUpdate.test.ts+1-1
...@@ -105,7 +105,7 @@ const makeHarness = Effect.fn("test.make_self_update_harness")(function* (...@@ -105,7 +105,7 @@ const makeHarness = Effect.fn("test.make_self_update_harness")(function* (
105 order.push("accept");105 order.push("accept");
106 return "launcher-id";106 return "launcher-id";
107 })),107 })),
108 prepareTrial: Effect.sync((): undefined => undefined),108 prepareTrial: Effect.undefined,
109 });109 });
110 const config = yield* ServerConfig.ServerConfig.pipe(110 const config = yield* ServerConfig.ServerConfig.pipe(
111 Effect.provide(ServerConfig.layerTest(process.cwd(), baseDir)),111 Effect.provide(ServerConfig.layerTest(process.cwd(), baseDir)),
apps/server/src/desktopUpdate/DesktopAppUpdate.ts+1-1
...@@ -106,7 +106,7 @@ export const make = Effect.fn("desktopUpdate.desktopAppUpdate.make")(function* (...@@ -106,7 +106,7 @@ export const make = Effect.fn("desktopUpdate.desktopAppUpdate.make")(function* (
106 ? emitStage(desktopUpdateProgressStage(report.state)).pipe(106 ? emitStage(desktopUpdateProgressStage(report.state)).pipe(
107 Effect.as(Option.none<DesktopUpdateStatusReport>()),107 Effect.as(Option.none<DesktopUpdateStatusReport>()),
108 )108 )
109 : Effect.succeed(Option.some(report)),109 : Effect.succeedSome(report),
110 ),110 ),
111 Stream.filterMap(111 Stream.filterMap(
112 Option.match({112 Option.match({
apps/server/src/device/DeviceHubProxy.ts+1-1
...@@ -142,7 +142,7 @@ const proxyWebSocket = Effect.fn("DeviceHubProxy.proxyWebSocket")(function* (...@@ -142,7 +142,7 @@ const proxyWebSocket = Effect.fn("DeviceHubProxy.proxyWebSocket")(function* (
142 pumpFrames(client, writeToUpstream),142 pumpFrames(client, writeToUpstream),
143 );143 );
144 }),144 }),
145 ).pipe(Effect.catchCause(() => Effect.void));145 ).pipe(Effect.ignoreCause);
146 return HttpServerResponse.empty();146 return HttpServerResponse.empty();
147});147});
148148
apps/server/src/device/DeviceService.ts+5-7
...@@ -906,13 +906,11 @@ export const makeWithHosts = Effect.fn("DeviceService.makeWithHosts")(function*...@@ -906,13 +906,11 @@ export const makeWithHosts = Effect.fn("DeviceService.makeWithHosts")(function*
906 lifecycleLock.withPermit(906 lifecycleLock.withPermit(
907 Effect.gen(function* () {907 Effect.gen(function* () {
908 if (!installTool)908 if (!installTool)
909 return yield* Effect.fail(909 return yield* new DeviceOperationError({
910 new DeviceOperationError({910 operation: "update device tool",
911 operation: "update device tool",911 reason: "request_failed",
912 reason: "request_failed",912 cause: new Error("Tool installation is unavailable in this device service."),
913 cause: new Error("Tool installation is unavailable in this device service."),913 });
914 }),
915 );
916 yield* installTool(tool);914 yield* installTool(tool);
917 return yield* inspect;915 return yield* inspect;
918 }),916 }),
apps/server/src/device/LocalDeviceHost.ts+2-2
...@@ -326,7 +326,7 @@ export const make = Effect.fn("LocalDeviceHost.make")(function* () {...@@ -326,7 +326,7 @@ export const make = Effect.fn("LocalDeviceHost.make")(function* () {
326 }326 }
327 }327 }
328 yield* fs.remove(hubStatePath(), { force: true }).pipe(Effect.ignore);328 yield* fs.remove(hubStatePath(), { force: true }).pipe(Effect.ignore);
329 }).pipe(Effect.catchCause(() => Effect.void));329 }).pipe(Effect.ignoreCause);
330330
331 const recordHub = (hub: HubProcess, hubTool: DeviceToolPaths) =>331 const recordHub = (hub: HubProcess, hubTool: DeviceToolPaths) =>
332 encodeHubStateFile({332 encodeHubStateFile({
...@@ -422,7 +422,7 @@ export const make = Effect.fn("LocalDeviceHost.make")(function* () {...@@ -422,7 +422,7 @@ export const make = Effect.fn("LocalDeviceHost.make")(function* () {
422 Stream.runForEach((line) =>422 Stream.runForEach((line) =>
423 Effect.logDebug("Device hub output", { pid: Number(hub.child.pid), output: line }),423 Effect.logDebug("Device hub output", { pid: Number(hub.child.pid), output: line }),
424 ),424 ),
425 Effect.catchCause(() => Effect.void),425 Effect.ignoreCause,
426 );426 );
427427
428 /**428 /**
apps/server/src/device/deviceToolMaintenance.ts+6-3
...@@ -128,9 +128,12 @@ const runMaintenance = Effect.fn("DeviceToolchain.maintenance")(function* (...@@ -128,9 +128,12 @@ const runMaintenance = Effect.fn("DeviceToolchain.maintenance")(function* (
128 ],128 ],
129 });129 });
130 if (result.code !== 0)130 if (result.code !== 0)
131 return yield* Effect.fail(131 return yield* new DeviceToolMaintenanceError({
132 new DeviceToolMaintenanceError({ operation, tool, exitCode: result.code, cause: result }),132 operation,
133 );133 tool,
134 exitCode: result.code,
135 cause: result,
136 });
134});137});
135138
136export const pruneLocalDeviceTools = Effect.fn("DeviceToolchain.prune")(function* (139export const pruneLocalDeviceTools = Effect.fn("DeviceToolchain.prune")(function* (
apps/server/src/diagnostics/TraceDiagnostics.ts+3-3
...@@ -420,8 +420,9 @@ export const make = Effect.gen(function* () {...@@ -420,8 +420,9 @@ export const make = Effect.gen(function* () {
420 const readAt = options.readAt ?? (yield* DateTime.now);420 const readAt = options.readAt ?? (yield* DateTime.now);
421 const slowSpanThresholdMs = options.slowSpanThresholdMs ?? DEFAULT_SLOW_SPAN_THRESHOLD_MS;421 const slowSpanThresholdMs = options.slowSpanThresholdMs ?? DEFAULT_SLOW_SPAN_THRESHOLD_MS;
422 const paths = toRotatedTracePaths(options.traceFilePath, options.maxFiles);422 const paths = toRotatedTracePaths(options.traceFilePath, options.maxFiles);
423 const results = yield* Effect.all(423 const results = yield* Effect.forEach(
424 paths.map((path) =>424 paths,
425 (path) =>
425 readTraceFile(fileSystem, path).pipe(426 readTraceFile(fileSystem, path).pipe(
426 Effect.tapError((cause) =>427 Effect.tapError((cause) =>
427 Effect.logWarning("Failed to read local trace file.").pipe(428 Effect.logWarning("Failed to read local trace file.").pipe(
...@@ -434,7 +435,6 @@ export const make = Effect.gen(function* () {...@@ -434,7 +435,6 @@ export const make = Effect.gen(function* () {
434 ),435 ),
435 Effect.result,436 Effect.result,
436 ),437 ),
437 ),
438 {438 {
439 concurrency: 1,439 concurrency: 1,
440 },440 },
apps/server/src/environment/ServerEnvironment.test.ts+1-2
...@@ -6,7 +6,6 @@ import * as Deferred from "effect/Deferred";...@@ -6,7 +6,6 @@ import * as Deferred from "effect/Deferred";
6import * as Effect from "effect/Effect";6import * as Effect from "effect/Effect";
7import * as FileSystem from "effect/FileSystem";7import * as FileSystem from "effect/FileSystem";
8import * as Layer from "effect/Layer";8import * as Layer from "effect/Layer";
9import * as Option from "effect/Option";
10import * as PlatformError from "effect/PlatformError";9import * as PlatformError from "effect/PlatformError";
11import * as Schema from "effect/Schema";10import * as Schema from "effect/Schema";
1211
...@@ -35,7 +34,7 @@ const makeServerEnvironmentLayer = (baseDir: string) =>...@@ -35,7 +34,7 @@ const makeServerEnvironmentLayer = (baseDir: string) =>
35const emptySecretStoreLayer = Layer.succeed(34const emptySecretStoreLayer = Layer.succeed(
36 ServerSecretStore.ServerSecretStore,35 ServerSecretStore.ServerSecretStore,
37 ServerSecretStore.ServerSecretStore.of({36 ServerSecretStore.ServerSecretStore.of({
38 get: () => Effect.succeed(Option.none()),37 get: () => Effect.succeedNone,
39 set: () => Effect.void,38 set: () => Effect.void,
40 create: () => Effect.void,39 create: () => Effect.void,
41 getOrCreateRandom: () => Effect.succeed(new Uint8Array()),40 getOrCreateRandom: () => Effect.succeed(new Uint8Array()),
apps/server/src/environment/ServerEnvironment.ts+6-7
...@@ -127,13 +127,12 @@ const makeIdentity = Effect.gen(function* () {...@@ -127,13 +127,12 @@ const makeIdentity = Effect.gen(function* () {
127 });127 });
128 yield* fileSystem.writeFileString(tempPath, `${value}\n`);128 yield* fileSystem.writeFileString(tempPath, `${value}\n`);
129 // Publish the completed file without replacing an ID created by another process.129 // Publish the completed file without replacing an ID created by another process.
130 yield* fileSystem130 yield* fileSystem.link(tempPath, destinationPath).pipe(
131 .link(tempPath, destinationPath)131 Effect.catchIf(
132 .pipe(132 (cause) => cause.reason._tag === "AlreadyExists",
133 Effect.catch((cause) =>133 () => Effect.void,
134 cause.reason._tag === "AlreadyExists" ? Effect.void : Effect.fail(cause),134 ),
135 ),135 );
136 );
137 if (mode === "recover") {136 if (mode === "recover") {
138 // Keep the recovery ID so delayed initializers also publish the same winner.137 // Keep the recovery ID so delayed initializers also publish the same winner.
139 yield* fileSystem.remove(tempPath);138 yield* fileSystem.remove(tempPath);
apps/server/src/environment/ServerEnvironmentLabel.ts+1-1
...@@ -128,7 +128,7 @@ const runFriendlyLabelCommand = Effect.fn("runFriendlyLabelCommand")(function* (...@@ -128,7 +128,7 @@ const runFriendlyLabelCommand = Effect.fn("runFriendlyLabelCommand")(function* (
128 cause,128 cause,
129 }),129 }),
130 ),130 ),
131 Effect.map(Option.some),131 Effect.asSome,
132 Effect.catchTags({132 Effect.catchTags({
133 ServerEnvironmentLabelCommandError: (error) =>133 ServerEnvironmentLabelCommandError: (error) =>
134 Effect.logDebug(error.message).pipe(134 Effect.logDebug(error.message).pipe(
apps/server/src/environment/ServerEnvironmentMachine.ts+2-2
...@@ -107,7 +107,7 @@ const readOptionalFile = Effect.fn("readOptionalFile")(function* (path: string)...@@ -107,7 +107,7 @@ const readOptionalFile = Effect.fn("readOptionalFile")(function* (path: string)
107 const fileSystem = yield* FileSystem.FileSystem;107 const fileSystem = yield* FileSystem.FileSystem;
108 return yield* fileSystem.readFileString(path).pipe(108 return yield* fileSystem.readFileString(path).pipe(
109 Effect.map(normalize),109 Effect.map(normalize),
110 Effect.catch(() => Effect.succeed(null)),110 Effect.orElseSucceed(() => null),
111 );111 );
112});112});
113113
...@@ -125,7 +125,7 @@ const runProbe = Effect.fn("runMachineProbe")(function* (input: {...@@ -125,7 +125,7 @@ const runProbe = Effect.fn("runMachineProbe")(function* (input: {
125 })125 })
126 .pipe(126 .pipe(
127 Effect.map((result) => (result.code === 0 ? normalize(result.stdout) : null)),127 Effect.map((result) => (result.code === 0 ? normalize(result.stdout) : null)),
128 Effect.catch(() => Effect.succeed(null)),128 Effect.orElseSucceed(() => null),
129 );129 );
130});130});
131131
apps/server/src/git/GitManager.ts+1-1
...@@ -2097,7 +2097,7 @@ export const make = Effect.gen(function* () {...@@ -2097,7 +2097,7 @@ export const make = Effect.gen(function* () {
2097 title: generated.title,2097 title: generated.title,
2098 bodyFile,2098 bodyFile,
2099 })2099 })
2100 .pipe(Effect.ensuring(fileSystem.remove(bodyFile).pipe(Effect.catch(() => Effect.void))));2100 .pipe(Effect.ensuring(fileSystem.remove(bodyFile).pipe(Effect.ignore)));
21012101
2102 const created = yield* findOpenPr(cwd, headContext);2102 const created = yield* findOpenPr(cwd, headContext);
2103 if (!created) {2103 if (!created) {
apps/server/src/git/linkCreatedPullRequest.test.ts+1-1
...@@ -83,7 +83,7 @@ const makeDependencies = (...@@ -83,7 +83,7 @@ const makeDependencies = (
83 Layer.mergeAll(83 Layer.mergeAll(
84 Layer.mock(ProjectionSnapshotQuery)({84 Layer.mock(ProjectionSnapshotQuery)({
85 getThreadShellById: () => Effect.succeed(Option.fromNullishOr(threadShell)),85 getThreadShellById: () => Effect.succeed(Option.fromNullishOr(threadShell)),
86 getProjectShellById: () => Effect.succeed(Option.some(project)),86 getProjectShellById: () => Effect.succeedSome(project),
87 }),87 }),
88 Layer.mock(OrchestrationEngineService)({88 Layer.mock(OrchestrationEngineService)({
89 readEvents: () => Stream.empty,89 readEvents: () => Stream.empty,
apps/server/src/keybindings.test.ts+3-3
...@@ -567,13 +567,13 @@ it.layer(NodeServices.layer)("keybindings", (it) => {...@@ -567,13 +567,13 @@ it.layer(NodeServices.layer)("keybindings", (it) => {
567 );567 );
568 yield* Effect.gen(function* () {568 yield* Effect.gen(function* () {
569 const keybindings = yield* Keybindings.Keybindings;569 const keybindings = yield* Keybindings.Keybindings;
570 yield* Effect.all(570 yield* Effect.forEach(
571 commands.map((command, index) =>571 commands,
572 (command, index) =>
572 keybindings.upsertKeybindingRule({573 keybindings.upsertKeybindingRule({
573 key: `mod+${String.fromCharCode(97 + index)}`,574 key: `mod+${String.fromCharCode(97 + index)}`,
574 command,575 command,
575 }),576 }),
576 ),
577 { concurrency: "unbounded", discard: true },577 { concurrency: "unbounded", discard: true },
578 );578 );
579 });579 });
apps/server/src/mcp/McpHttpServer.test.ts+1-2
...@@ -6,7 +6,6 @@ import * as Deferred from "effect/Deferred";...@@ -6,7 +6,6 @@ import * as Deferred from "effect/Deferred";
6import * as Effect from "effect/Effect";6import * as Effect from "effect/Effect";
7import * as FileSystem from "effect/FileSystem";7import * as FileSystem from "effect/FileSystem";
8import * as Layer from "effect/Layer";8import * as Layer from "effect/Layer";
9import * as Option from "effect/Option";
10import * as Path from "effect/Path";9import * as Path from "effect/Path";
11import * as Schema from "effect/Schema";10import * as Schema from "effect/Schema";
12import * as Stream from "effect/Stream";11import * as Stream from "effect/Stream";
...@@ -56,7 +55,7 @@ const PullRequestsTestLayer = McpHttpServer.PullRequestsToolkitRegistrationLive....@@ -56,7 +55,7 @@ const PullRequestsTestLayer = McpHttpServer.PullRequestsToolkitRegistrationLive.
56 Layer.provide(55 Layer.provide(
57 Layer.mergeAll(56 Layer.mergeAll(
58 Layer.mock(ProjectionSnapshotQuery)({57 Layer.mock(ProjectionSnapshotQuery)({
59 getThreadShellById: () => Effect.succeed(Option.none()),58 getThreadShellById: () => Effect.succeedNone,
60 }),59 }),
61 Layer.mock(OrchestrationEngineService)({}),60 Layer.mock(OrchestrationEngineService)({}),
62 NodeServices.layer,61 NodeServices.layer,
apps/server/src/mcp/McpInvocationContext.ts+8-6
...@@ -47,9 +47,11 @@ const missingCapability = (...@@ -47,9 +47,11 @@ const missingCapability = (
47export const requireMcpCapability = <const C extends McpCapability>(47export const requireMcpCapability = <const C extends McpCapability>(
48 capability: C,48 capability: C,
49): Effect.Effect<McpInvocationScope, McpCapabilityError<C>, McpInvocationContext> =>49): Effect.Effect<McpInvocationScope, McpCapabilityError<C>, McpInvocationContext> =>
50 Effect.flatMap(McpInvocationContext, (invocation) =>50 McpInvocationContext.pipe(
51 invocation.capabilities.has(capability)51 Effect.filterOrFail(
52 ? Effect.succeed(invocation)52 (invocation) => invocation.capabilities.has(capability),
53 : // The conditional type narrows what the literal argument decided at runtime.53 // The conditional type narrows what the literal argument decided at runtime.
54 Effect.fail(missingCapability(invocation, capability) as McpCapabilityError<C>),54 (invocation) => missingCapability(invocation, capability) as McpCapabilityError<C>,
55 ).pipe(Effect.withSpan("mcp.requireCapability"));55 ),
56 Effect.withSpan("mcp.requireCapability"),
57 );
apps/server/src/mcp/McpSessionRegistry.ts+1-1
...@@ -230,7 +230,7 @@ export const issueActiveMcpCredential = (...@@ -230,7 +230,7 @@ export const issueActiveMcpCredential = (
230 ? activeMcpSessionRegistry230 ? activeMcpSessionRegistry
231 .revokeThread(request.threadId)231 .revokeThread(request.threadId)
232 .pipe(Effect.andThen(activeMcpSessionRegistry.issue(request)))232 .pipe(Effect.andThen(activeMcpSessionRegistry.issue(request)))
233 : Effect.sync((): McpIssuedCredential | undefined => undefined);233 : Effect.undefined;
234234
235/**235/**
236 * Refreshes the liveness of a thread's MCP credential. Called on every provider236 * Refreshes the liveness of a thread's MCP credential. Called on every provider
apps/server/src/mcp/toolkits/preview/handlers.ts+6-5
...@@ -87,7 +87,7 @@ const invoke = Effect.fn("PreviewToolkit.invoke")(function* <A>(...@@ -87,7 +87,7 @@ const invoke = Effect.fn("PreviewToolkit.invoke")(function* <A>(
87 updateCurrentTab: false,87 updateCurrentTab: false,
88 ...(statusTabId === undefined ? {} : { tabId: statusTabId }),88 ...(statusTabId === undefined ? {} : { tabId: statusTabId }),
89 })89 })
90 .pipe(Effect.catch(() => Effect.succeed(null)));90 .pipe(Effect.orElseSucceed(() => null));
91 return {91 return {
92 result,92 result,
93 ...(page?.url && /^https?:\/\//i.test(page.url) && page.url.length <= 409693 ...(page?.url && /^https?:\/\//i.test(page.url) && page.url.length <= 4096
...@@ -175,10 +175,11 @@ export const claimPreviewRecording = Effect.fn("PreviewToolkit.claimRecording")(...@@ -175,10 +175,11 @@ export const claimPreviewRecording = Effect.fn("PreviewToolkit.claimRecording")(
175 yield* fileSystem.rename(currentPath, finalPath);175 yield* fileSystem.rename(currentPath, finalPath);
176 }).pipe(176 }).pipe(
177 // Another stop may already have claimed this exact upload for this thread.177 // Another stop may already have claimed this exact upload for this thread.
178 Effect.catch((cause) =>178 Effect.catchIf(
179 cause._tag !== "PreviewAutomationRecordingTransferError" && cause.reason._tag === "NotFound"179 (cause) =>
180 ? validateFile(finalPath)180 cause._tag !== "PreviewAutomationRecordingTransferError" &&
181 : Effect.fail(cause),181 cause.reason._tag === "NotFound",
182 () => validateFile(finalPath),
182 ),183 ),
183 Effect.mapError((cause) => new PreviewAutomationRecordingTransferError({ threadId, cause })),184 Effect.mapError((cause) => new PreviewAutomationRecordingTransferError({ threadId, cause })),
184 );185 );
apps/server/src/orchestration/Layers/CheckpointReactor.ts+31-37
...@@ -97,12 +97,12 @@ const make = Effect.gen(function* () {...@@ -97,12 +97,12 @@ const make = Effect.gen(function* () {
97 const entryRefreshWorker = yield* makeDrainableWorker((cwd: string) =>97 const entryRefreshWorker = yield* makeDrainableWorker((cwd: string) =>
98 Effect.sync(() => queuedEntryRefreshes.delete(cwd)).pipe(98 Effect.sync(() => queuedEntryRefreshes.delete(cwd)).pipe(
99 Effect.andThen(workspaceEntries.refresh(cwd)),99 Effect.andThen(workspaceEntries.refresh(cwd)),
100 Effect.catchCause((cause) =>100 Effect.catchCauseIf(
101 Cause.hasInterruptsOnly(cause)101 (cause) => !Cause.hasInterruptsOnly(cause),
102 ? Effect.failCause(cause)102 () =>
103 : Effect.logWarning("failed to refresh checkpoint workspace entries", {103 Effect.logWarning("failed to refresh checkpoint workspace entries", {
104 cwd,104 cwd,
105 }),105 }),
106 ),106 ),
107 ),107 ),
108 );108 );
...@@ -625,15 +625,14 @@ const make = Effect.gen(function* () {...@@ -625,15 +625,14 @@ const make = Effect.gen(function* () {
625 branch: checkedOutBranch,625 branch: checkedOutBranch,
626 });626 });
627 }).pipe(627 }).pipe(
628 Effect.catchCause((cause) => {628 Effect.catchCauseIf(
629 if (Cause.hasInterruptsOnly(cause)) {629 (cause) => !Cause.hasInterruptsOnly(cause),
630 return Effect.failCause(cause);630 (cause) =>
631 }631 Effect.logWarning("failed to follow worktree branch drift", {
632 return Effect.logWarning("failed to follow worktree branch drift", {632 threadId: input.threadId,
633 threadId: input.threadId,633 cause: Cause.pretty(cause),
634 cause: Cause.pretty(cause),634 }),
635 });635 ),
636 }),
637 );636 );
638 });637 });
639638
...@@ -643,12 +642,12 @@ const make = Effect.gen(function* () {...@@ -643,12 +642,12 @@ const make = Effect.gen(function* () {
643 const statusRefreshWorker = yield* makeDrainableWorker(642 const statusRefreshWorker = yield* makeDrainableWorker(
644 (event: Extract<ProviderRuntimeEvent, { type: "turn.completed" }>) =>643 (event: Extract<ProviderRuntimeEvent, { type: "turn.completed" }>) =>
645 refreshLocalGitStatusFromTurnCompletion(event).pipe(644 refreshLocalGitStatusFromTurnCompletion(event).pipe(
646 Effect.catchCause((cause) =>645 Effect.catchCauseIf(
647 Cause.hasInterruptsOnly(cause)646 (cause) => !Cause.hasInterruptsOnly(cause),
648 ? Effect.failCause(cause)647 () =>
649 : Effect.logWarning("failed to refresh git status after turn completion", {648 Effect.logWarning("failed to refresh git status after turn completion", {
650 threadId: event.threadId,649 threadId: event.threadId,
651 }),650 }),
652 ),651 ),
653 ),652 ),
654 );653 );
...@@ -749,11 +748,7 @@ const make = Effect.gen(function* () {...@@ -749,11 +748,7 @@ const make = Effect.gen(function* () {
749 for (const candidate of paths) {748 for (const candidate of paths) {
750 const otherCwd = yield* fileSystem749 const otherCwd = yield* fileSystem
751 .realPath(candidate)750 .realPath(candidate)
752 .pipe(751 .pipe(Effect.catchReason("PlatformError", "NotFound", () => Effect.succeed(null)));
753 Effect.catch((error) =>
754 error.reason._tag === "NotFound" ? Effect.succeed(null) : Effect.fail(error),
755 ),
756 );
757 if (otherCwd === null) continue;752 if (otherCwd === null) continue;
758 const isWithin = (parent: string, child: string) => {753 const isWithin = (parent: string, child: string) => {
759 const relative = path.relative(parent, child);754 const relative = path.relative(parent, child);
...@@ -791,7 +786,7 @@ const make = Effect.gen(function* () {...@@ -791,7 +786,7 @@ const make = Effect.gen(function* () {
791 preferSessionRuntime: true,786 preferSessionRuntime: true,
792 }).pipe(787 }).pipe(
793 Effect.catch((error) =>788 Effect.catch((error) =>
794 event.payload.restoreFiles === false ? Effect.succeed(undefined) : Effect.fail(error),789 event.payload.restoreFiles === false ? Effect.undefined : Effect.fail(error),
795 ),790 ),
796 );791 );
797792
...@@ -1014,16 +1009,15 @@ const make = Effect.gen(function* () {...@@ -1014,16 +1009,15 @@ const make = Effect.gen(function* () {
10141009
1015 const processInputSafely = (input: ReactorInput) =>1010 const processInputSafely = (input: ReactorInput) =>
1016 processInput(input).pipe(1011 processInput(input).pipe(
1017 Effect.catchCause((cause) => {1012 Effect.catchCauseIf(
1018 if (Cause.hasInterruptsOnly(cause)) {1013 (cause) => !Cause.hasInterruptsOnly(cause),
1019 return Effect.failCause(cause);1014 (cause) =>
1020 }1015 Effect.logWarning("checkpoint reactor failed to process input", {
1021 return Effect.logWarning("checkpoint reactor failed to process input", {1016 source: input.source,
1022 source: input.source,1017 eventType: input.event.type,
1023 eventType: input.event.type,1018 cause: Cause.pretty(cause),
1024 cause: Cause.pretty(cause),1019 }),
1025 });1020 ),
1026 }),
1027 );1021 );
10281022
1029 const worker = yield* makeDrainableWorker(processInputSafely);1023 const worker = yield* makeDrainableWorker(processInputSafely);
apps/server/src/orchestration/Layers/OrchestrationEngine.test.ts+8-8
...@@ -446,18 +446,18 @@ describe("OrchestrationEngine", () => {...@@ -446,18 +446,18 @@ describe("OrchestrationEngine", () => {
446 Effect.succeed({ snapshotSequence: projectionSnapshot.snapshotSequence }),446 Effect.succeed({ snapshotSequence: projectionSnapshot.snapshotSequence }),
447 getCounts: () => Effect.succeed({ projectCount: 1, threadCount: 1 }),447 getCounts: () => Effect.succeed({ projectCount: 1, threadCount: 1 }),
448 getEventReplayStats: () => Effect.die("unused"),448 getEventReplayStats: () => Effect.die("unused"),
449 getActiveProjectByWorkspaceRoot: () => Effect.succeed(Option.none()),449 getActiveProjectByWorkspaceRoot: () => Effect.succeedNone,
450 getProjectShellById: () => Effect.succeed(Option.none()),450 getProjectShellById: () => Effect.succeedNone,
451 getProjectShells: () => Effect.succeed([]),451 getProjectShells: () => Effect.succeed([]),
452 getFirstActiveThreadIdByProjectId: () => Effect.succeed(Option.none()),452 getFirstActiveThreadIdByProjectId: () => Effect.succeedNone,
453 getImportedAgentSessionSources: () => Effect.die("unused"),453 getImportedAgentSessionSources: () => Effect.die("unused"),
454 getThreadCheckpointContext: () => Effect.succeed(Option.none()),454 getThreadCheckpointContext: () => Effect.succeedNone,
455 getFullThreadDiffContext: () => Effect.succeed(Option.none()),455 getFullThreadDiffContext: () => Effect.succeedNone,
456 getThreadRuntimeContext: () => Effect.die("unused"),456 getThreadRuntimeContext: () => Effect.die("unused"),
457 getTurnStartMessage: () => Effect.die("unused"),457 getTurnStartMessage: () => Effect.die("unused"),
458 getThreadShellById: () => Effect.succeed(Option.none()),458 getThreadShellById: () => Effect.succeedNone,
459 getThreadDetailById: () => Effect.succeed(Option.none()),459 getThreadDetailById: () => Effect.succeedNone,
460 getThreadDetailSnapshot: () => Effect.succeed(Option.none()),460 getThreadDetailSnapshot: () => Effect.succeedNone,
461 searchThreads: () => Effect.succeed({ matches: [] }),461 searchThreads: () => Effect.succeed({ matches: [] }),
462 }),462 }),
463 ),463 ),
apps/server/src/orchestration/Layers/OrchestrationEngine.ts+1-1
...@@ -400,7 +400,7 @@ const makeOrchestrationEngine = Effect.gen(function* () {...@@ -400,7 +400,7 @@ const makeOrchestrationEngine = Effect.gen(function* () {
400 status: "rejected",400 status: "rejected",
401 error: error.message,401 error: error.message,
402 })402 })
403 .pipe(Effect.catch(() => Effect.void));403 .pipe(Effect.ignore);
404 }404 }
405 }405 }
406406
apps/server/src/orchestration/Layers/ProviderCommandReactor.ts+29-38
...@@ -511,14 +511,14 @@ const make = Effect.gen(function* () {...@@ -511,14 +511,14 @@ const make = Effect.gen(function* () {
511 Effect.andThen(511 Effect.andThen(
512 gitWorkflow.createWorktree({ cwd, refName: branch, path: worktreePath }, { submodules }),512 gitWorkflow.createWorktree({ cwd, refName: branch, path: worktreePath }, { submodules }),
513 ),513 ),
514 Effect.catchCause((cause) =>514 Effect.catchCauseIf(
515 Cause.hasInterruptsOnly(cause)515 (cause) => !Cause.hasInterruptsOnly(cause),
516 ? Effect.failCause(cause)516 (cause) =>
517 : Effect.logWarning("provider command reactor failed to recreate worktree", {517 Effect.logWarning("provider command reactor failed to recreate worktree", {
518 threadId: thread.id,518 threadId: thread.id,
519 worktreePath,519 worktreePath,
520 cause: Cause.pretty(cause),520 cause: Cause.pretty(cause),
521 }),521 }),
522 ),522 ),
523 );523 );
524 });524 });
...@@ -1154,15 +1154,14 @@ const make = Effect.gen(function* () {...@@ -1154,15 +1154,14 @@ const make = Effect.gen(function* () {
1154 return;1154 return;
1155 }1155 }
1156 const result = yield* regenerateThreadTitle(event, requestId).pipe(1156 const result = yield* regenerateThreadTitle(event, requestId).pipe(
1157 Effect.catchCause((cause) => {1157 Effect.catchCauseIf(
1158 if (Cause.hasInterruptsOnly(cause)) {1158 (cause) => !Cause.hasInterruptsOnly(cause),
1159 return Effect.failCause(cause);1159 (cause) =>
1160 }1160 Effect.logWarning("provider command reactor failed to regenerate thread title", {
1161 return Effect.logWarning("provider command reactor failed to regenerate thread title", {1161 threadId: event.payload.threadId,
1162 threadId: event.payload.threadId,1162 cause: Cause.pretty(cause),
1163 cause: Cause.pretty(cause),1163 }).pipe(Effect.as({ _tag: "Completed", title: undefined } as const)),
1164 }).pipe(Effect.as({ _tag: "Completed", title: undefined } as const));1164 ),
1165 }),
1166 );1165 );
1167 if (result._tag === "Superseded") {1166 if (result._tag === "Superseded") {
1168 return;1167 return;
...@@ -1174,34 +1173,26 @@ const make = Effect.gen(function* () {...@@ -1174,34 +1173,26 @@ const make = Effect.gen(function* () {
1174 ...(result.title !== undefined ? { title: result.title } : {}),1173 ...(result.title !== undefined ? { title: result.title } : {}),
1175 };1174 };
1176 yield* dispatchThreadTitleRegenerationCompletion(completion).pipe(1175 yield* dispatchThreadTitleRegenerationCompletion(completion).pipe(
1177 Effect.catchCause((cause) => {1176 Effect.catchCauseIf(
1178 if (Cause.hasInterruptsOnly(cause)) {1177 (cause) => !Cause.hasInterruptsOnly(cause),
1179 return Effect.failCause(cause);1178 (cause) =>
1180 }1179 Effect.logWarning("provider command reactor retrying title regeneration completion", {
1181 return Effect.logWarning(
1182 "provider command reactor retrying title regeneration completion",
1183 {
1184 threadId: event.payload.threadId,1180 threadId: event.payload.threadId,
1185 cause: Cause.pretty(cause),1181 cause: Cause.pretty(cause),
1186 },1182 }).pipe(Effect.andThen(dispatchThreadTitleRegenerationCompletion(completion))),
1187 ).pipe(Effect.andThen(dispatchThreadTitleRegenerationCompletion(completion)));1183 ),
1188 }),
1189 );1184 );
1190 },1185 },
1191 (effect, event) =>1186 (effect, event) =>
1192 effect.pipe(1187 effect.pipe(
1193 Effect.catchCause((cause) => {1188 Effect.catchCauseIf(
1194 if (Cause.hasInterruptsOnly(cause)) {1189 (cause) => !Cause.hasInterruptsOnly(cause),
1195 return Effect.failCause(cause);1190 (cause) =>
1196 }1191 Effect.logWarning("provider command reactor failed to complete title regeneration", {
1197 return Effect.logWarning(
1198 "provider command reactor failed to complete title regeneration",
1199 {
1200 threadId: event.payload.threadId,1192 threadId: event.payload.threadId,
1201 cause: Cause.pretty(cause),1193 cause: Cause.pretty(cause),
1202 },1194 }),
1203 );1195 ),
1204 }),
1205 ),1196 ),
1206 );1197 );
1207 const threadTitleRegenerationWorker = yield* makeDrainableWorker(1198 const threadTitleRegenerationWorker = yield* makeDrainableWorker(
...@@ -1496,7 +1487,7 @@ const make = Effect.gen(function* () {...@@ -1496,7 +1487,7 @@ const make = Effect.gen(function* () {
1496 interactionMode: event.payload.interactionMode,1487 interactionMode: event.payload.interactionMode,
1497 createdAt: event.payload.createdAt,1488 createdAt: event.payload.createdAt,
1498 }).pipe(1489 }).pipe(
1499 Effect.map(Option.some),1490 Effect.asSome,
1500 Effect.catchCause((cause) => handleTurnStartFailure(cause).pipe(Effect.as(Option.none()))),1491 Effect.catchCause((cause) => handleTurnStartFailure(cause).pipe(Effect.as(Option.none()))),
1501 );1492 );
15021493
apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts+10-11
...@@ -2671,17 +2671,16 @@ const make = Effect.gen(function* () {...@@ -2671,17 +2671,16 @@ const make = Effect.gen(function* () {
2671 (source: string, event: { readonly eventId: string; readonly type: string }) =>2671 (source: string, event: { readonly eventId: string; readonly type: string }) =>
2672 <E, R>(effect: Effect.Effect<void, E, R>) =>2672 <E, R>(effect: Effect.Effect<void, E, R>) =>
2673 effect.pipe(2673 effect.pipe(
2674 Effect.catchCause((cause) => {2674 Effect.catchCauseIf(
2675 if (Cause.hasInterruptsOnly(cause)) {2675 (cause) => !Cause.hasInterruptsOnly(cause),
2676 return Effect.failCause(cause);2676 (cause) =>
2677 }2677 Effect.logWarning("provider runtime ingestion failed to process event", {
2678 return Effect.logWarning("provider runtime ingestion failed to process event", {2678 source,
2679 source,2679 eventId: event.eventId,
2680 eventId: event.eventId,2680 eventType: event.type,
2681 eventType: event.type,2681 cause: Cause.pretty(cause),
2682 cause: Cause.pretty(cause),2682 }),
2683 });2683 ),
2684 }),
2685 );2684 );
26862685
2687 const worker = yield* makeDrainableWorker((input: RuntimeIngestionInput) =>2686 const worker = yield* makeDrainableWorker((input: RuntimeIngestionInput) =>
apps/server/src/orchestration/Layers/ThreadDeletionReactor.ts+17-19
...@@ -27,15 +27,14 @@ export const logCleanupCauseUnlessInterrupted = <R, E>({...@@ -27,15 +27,14 @@ export const logCleanupCauseUnlessInterrupted = <R, E>({
27 readonly threadId: ThreadDeletedEvent["payload"]["threadId"];27 readonly threadId: ThreadDeletedEvent["payload"]["threadId"];
28}): Effect.Effect<void, E, R> =>28}): Effect.Effect<void, E, R> =>
29 effect.pipe(29 effect.pipe(
30 Effect.catchCause((cause) => {30 Effect.catchCauseIf(
31 if (Cause.hasInterruptsOnly(cause)) {31 (cause) => !Cause.hasInterruptsOnly(cause),
32 return Effect.failCause(cause);32 (cause) =>
33 }33 Effect.logDebug(message, {
34 return Effect.logDebug(message, {34 threadId,
35 threadId,35 cause: Cause.pretty(cause),
36 cause: Cause.pretty(cause),36 }),
37 });37 ),
38 }),
39 );38 );
4039
41const make = Effect.gen(function* () {40const make = Effect.gen(function* () {
...@@ -67,16 +66,15 @@ const make = Effect.gen(function* () {...@@ -67,16 +66,15 @@ const make = Effect.gen(function* () {
6766
68 const processThreadDeletedSafely = (event: ThreadDeletedEvent) =>67 const processThreadDeletedSafely = (event: ThreadDeletedEvent) =>
69 processThreadDeleted(event).pipe(68 processThreadDeleted(event).pipe(
70 Effect.catchCause((cause) => {69 Effect.catchCauseIf(
71 if (Cause.hasInterruptsOnly(cause)) {70 (cause) => !Cause.hasInterruptsOnly(cause),
72 return Effect.failCause(cause);71 (cause) =>
73 }72 Effect.logWarning("thread deletion reactor failed to process event", {
74 return Effect.logWarning("thread deletion reactor failed to process event", {73 eventType: event.type,
75 eventType: event.type,74 threadId: event.payload.threadId,
76 threadId: event.payload.threadId,75 cause: Cause.pretty(cause),
77 cause: Cause.pretty(cause),76 }),
78 });77 ),
79 }),
80 );78 );
8179
82 const worker = yield* makeDrainableWorker(processThreadDeletedSafely);80 const worker = yield* makeDrainableWorker(processThreadDeletedSafely);
apps/server/src/orchestration/PullRequestSyncReactor.ts+6-6
...@@ -259,12 +259,12 @@ export const make = Effect.gen(function* () {...@@ -259,12 +259,12 @@ export const make = Effect.gen(function* () {
259 Effect.map((stack) => ({259 Effect.map((stack) => ({
260 stack: stack === null ? null : ({ kind: "native", ...stack } as const),260 stack: stack === null ? null : ({ kind: "native", ...stack } as const),
261 })),261 })),
262 Effect.catchCause((cause) =>262 Effect.catchCauseIf(
263 Cause.hasInterruptsOnly(cause)263 (cause) => !Cause.hasInterruptsOnly(cause),
264 ? Effect.failCause(cause)264 () =>
265 : Effect.logWarning("pull request stack lookup failed", {265 Effect.logWarning("pull request stack lookup failed", {
266 key,266 key,
267 }).pipe(Effect.as(null)),267 }).pipe(Effect.as(null)),
268 ),268 ),
269 )269 )
270 : null;270 : null;
apps/server/src/orchestration/ThreadPullRequestReactor.ts+30-30
...@@ -207,16 +207,16 @@ export const make = Effect.gen(function* () {...@@ -207,16 +207,16 @@ export const make = Effect.gen(function* () {
207 }207 }
208 return { thread, branchPullRequest, replacement };208 return { thread, branchPullRequest, replacement };
209 }).pipe(209 }).pipe(
210 Effect.catchCause((cause) =>210 Effect.catchCauseIf(
211 Cause.hasInterruptsOnly(cause)211 (cause) => !Cause.hasInterruptsOnly(cause),
212 ? Effect.failCause(cause)212 (cause) =>
213 : Effect.logWarning("thread pull request discovery failed", {213 Effect.logWarning("thread pull request discovery failed", {
214 threadId: thread.id,214 threadId: thread.id,
215 cause: Cause.pretty(cause),215 cause: Cause.pretty(cause),
216 }).pipe(216 }).pipe(
217 Effect.tap(() => Effect.sync(() => failBackfill([thread]))),217 Effect.tap(() => Effect.sync(() => failBackfill([thread]))),
218 Effect.as(null),218 Effect.as(null),
219 ),219 ),
220 ),220 ),
221 ),221 ),
222 );222 );
...@@ -273,25 +273,25 @@ export const make = Effect.gen(function* () {...@@ -273,25 +273,25 @@ export const make = Effect.gen(function* () {
273 OrchestrationCommandInvariantError: () =>273 OrchestrationCommandInvariantError: () =>
274 Effect.sync(() => finishBackfill([thread])),274 Effect.sync(() => finishBackfill([thread])),
275 }),275 }),
276 Effect.catchCause((cause) =>276 Effect.catchCauseIf(
277 Cause.hasInterruptsOnly(cause)277 (cause) => !Cause.hasInterruptsOnly(cause),
278 ? Effect.failCause(cause)278 (cause) =>
279 : Effect.logWarning("thread pull request update failed", {279 Effect.logWarning("thread pull request update failed", {
280 threadId: thread.id,280 threadId: thread.id,
281 cause: Cause.pretty(cause),281 cause: Cause.pretty(cause),
282 }).pipe(Effect.tap(() => Effect.sync(() => failBackfill([thread])))),282 }).pipe(Effect.tap(() => Effect.sync(() => failBackfill([thread])))),
283 ),283 ),
284 ),284 ),
285 { discard: true },285 { discard: true },
286 );286 );
287 }).pipe(287 }).pipe(
288 Effect.catchCause((cause) =>288 Effect.catchCauseIf(
289 Cause.hasInterruptsOnly(cause)289 (cause) => !Cause.hasInterruptsOnly(cause),
290 ? Effect.failCause(cause)290 (cause) =>
291 : Effect.logWarning("thread branch pull request lookup failed", {291 Effect.logWarning("thread branch pull request lookup failed", {
292 threadIds: group.map((thread) => thread.id),292 threadIds: group.map((thread) => thread.id),
293 cause: Cause.pretty(cause),293 cause: Cause.pretty(cause),
294 }).pipe(Effect.tap(() => Effect.sync(() => failBackfill(group)))),294 }).pipe(Effect.tap(() => Effect.sync(() => failBackfill(group)))),
295 ),295 ),
296 ),296 ),
297 { concurrency: 8, discard: true },297 { concurrency: 8, discard: true },
...@@ -300,12 +300,12 @@ export const make = Effect.gen(function* () {...@@ -300,12 +300,12 @@ export const make = Effect.gen(function* () {
300300
301 const worker = yield* makeDrainableWorker((request: RefreshRequest) =>301 const worker = yield* makeDrainableWorker((request: RefreshRequest) =>
302 synchronize(request).pipe(302 synchronize(request).pipe(
303 Effect.catchCause((cause) =>303 Effect.catchCauseIf(
304 Cause.hasInterruptsOnly(cause)304 (cause) => !Cause.hasInterruptsOnly(cause),
305 ? Effect.failCause(cause)305 (cause) =>
306 : Effect.logWarning("thread pull request refresh failed", {306 Effect.logWarning("thread pull request refresh failed", {
307 cause: Cause.pretty(cause),307 cause: Cause.pretty(cause),
308 }),308 }),
309 ),309 ),
310 ),310 ),
311 );311 );
apps/server/src/orchestration/ThreadSettlementReactor.ts+20-20
...@@ -137,13 +137,13 @@ export const make = Effect.gen(function* () {...@@ -137,13 +137,13 @@ export const make = Effect.gen(function* () {
137 },137 },
138 (effect, thread) =>138 (effect, thread) =>
139 effect.pipe(139 effect.pipe(
140 Effect.catchCause((cause) =>140 Effect.catchCauseIf(
141 Cause.hasInterruptsOnly(cause)141 (cause) => !Cause.hasInterruptsOnly(cause),
142 ? Effect.failCause(cause)142 (cause) =>
143 : Effect.logWarning("automatic thread settlement skipped", {143 Effect.logWarning("automatic thread settlement skipped", {
144 threadId: thread.id,144 threadId: thread.id,
145 cause: Cause.pretty(cause),145 cause: Cause.pretty(cause),
146 }).pipe(Effect.as(null)),146 }).pipe(Effect.as(null)),
147 ),147 ),
148 ),148 ),
149 );149 );
...@@ -305,13 +305,13 @@ export const make = Effect.gen(function* () {...@@ -305,13 +305,13 @@ export const make = Effect.gen(function* () {
305 discard: true,305 discard: true,
306 });306 });
307 }).pipe(307 }).pipe(
308 Effect.catchCause((cause) =>308 Effect.catchCauseIf(
309 Cause.hasInterruptsOnly(cause)309 (cause) => !Cause.hasInterruptsOnly(cause),
310 ? Effect.failCause(cause)310 (cause) =>
311 : Effect.logWarning("automatic thread settlement skipped", {311 Effect.logWarning("automatic thread settlement skipped", {
312 threadIds: group.map((thread) => thread.id),312 threadIds: group.map((thread) => thread.id),
313 cause: Cause.pretty(cause),313 cause: Cause.pretty(cause),
314 }),314 }),
315 ),315 ),
316 ),316 ),
317 { concurrency: 8, discard: true },317 { concurrency: 8, discard: true },
...@@ -323,12 +323,12 @@ export const make = Effect.gen(function* () {...@@ -323,12 +323,12 @@ export const make = Effect.gen(function* () {
323 threadId?: ThreadId,323 threadId?: ThreadId,
324 ) =>324 ) =>
325 sweep(mergedPullRequest, threadId).pipe(325 sweep(mergedPullRequest, threadId).pipe(
326 Effect.catchCause((cause) =>326 Effect.catchCauseIf(
327 Cause.hasInterruptsOnly(cause)327 (cause) => !Cause.hasInterruptsOnly(cause),
328 ? Effect.failCause(cause)328 (cause) =>
329 : Effect.logWarning("automatic thread settlement sweep failed", {329 Effect.logWarning("automatic thread settlement sweep failed", {
330 cause: Cause.pretty(cause),330 cause: Cause.pretty(cause),
331 }),331 }),
332 ),332 ),
333 );333 );
334 const worker = yield* makeDrainableWorker((threadId: ThreadId | undefined) =>334 const worker = yield* makeDrainableWorker((threadId: ThreadId | undefined) =>
apps/server/src/orchestration/commandInvariants.ts+14-18
...@@ -119,15 +119,13 @@ export function requireThreadArchived(input: {...@@ -119,15 +119,13 @@ export function requireThreadArchived(input: {
119 readonly threadId: ThreadId;119 readonly threadId: ThreadId;
120}): Effect.Effect<OrchestrationThread, OrchestrationCommandInvariantError> {120}): Effect.Effect<OrchestrationThread, OrchestrationCommandInvariantError> {
121 return requireThread(input).pipe(121 return requireThread(input).pipe(
122 Effect.flatMap((thread) =>122 Effect.filterOrFail(
123 thread.archivedAt !== null123 (thread) => thread.archivedAt !== null,
124 ? Effect.succeed(thread)124 () =>
125 : Effect.fail(125 invariantError(
126 invariantError(126 input.command.type,
127 input.command.type,127 `Thread '${input.threadId}' is not archived for command '${input.command.type}'.`,
128 `Thread '${input.threadId}' is not archived for command '${input.command.type}'.`,128 ),
129 ),
130 ),
131 ),129 ),
132 );130 );
133}131}
...@@ -138,15 +136,13 @@ export function requireThreadNotArchived(input: {...@@ -138,15 +136,13 @@ export function requireThreadNotArchived(input: {
138 readonly threadId: ThreadId;136 readonly threadId: ThreadId;
139}): Effect.Effect<OrchestrationThread, OrchestrationCommandInvariantError> {137}): Effect.Effect<OrchestrationThread, OrchestrationCommandInvariantError> {
140 return requireThread(input).pipe(138 return requireThread(input).pipe(
141 Effect.flatMap((thread) =>139 Effect.filterOrFail(
142 thread.archivedAt === null140 (thread) => thread.archivedAt === null,
143 ? Effect.succeed(thread)141 () =>
144 : Effect.fail(142 invariantError(
145 invariantError(143 input.command.type,
146 input.command.type,144 `Thread '${input.threadId}' is already archived and cannot handle command '${input.command.type}'.`,
147 `Thread '${input.threadId}' is already archived and cannot handle command '${input.command.type}'.`,145 ),
148 ),
149 ),
150 ),146 ),
151 );147 );
152}148}
apps/server/src/orchestration/decider.pullRequests.test.ts+1-1
...@@ -295,7 +295,7 @@ it.layer(NodeServices.layer)("pull request link decider", (it) => {...@@ -295,7 +295,7 @@ it.layer(NodeServices.layer)("pull request link decider", (it) => {
295 for (const planned of events) {295 for (const planned of events) {
296 const event = { ...planned, sequence: model.snapshotSequence + 1 };296 const event = { ...planned, sequence: model.snapshotSequence + 1 };
297 const encoded = yield* Schema.encodeEffect(OrchestrationEvent)(event);297 const encoded = yield* Schema.encodeEffect(OrchestrationEvent)(event);
298 const decoded = yield* Schema.decodeUnknownEffect(OrchestrationEvent)(encoded);298 const decoded = yield* Schema.decodeEffect(OrchestrationEvent)(encoded);
299 // Older detail-event unions must never receive the new PR discriminants.299 // Older detail-event unions must never receive the new PR discriminants.
300 expect(isThreadDetailEvent(decoded)).toBe(false);300 expect(isThreadDetailEvent(decoded)).toBe(false);
301 model = yield* projectEvent(model, decoded);301 model = yield* projectEvent(model, decoded);
apps/server/src/orchestration/decider.ts+28-42
...@@ -488,12 +488,10 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand"...@@ -488,12 +488,10 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand"
488 command.type === "thread.auto-settle" &&488 command.type === "thread.auto-settle" &&
489 (thread.settledOverride !== null || thread.autoSettleDisabledAt != null)489 (thread.settledOverride !== null || thread.autoSettleDisabledAt != null)
490 ) {490 ) {
491 return yield* Effect.fail(491 return yield* new OrchestrationCommandInvariantError({
492 new OrchestrationCommandInvariantError({492 commandType: command.type,
493 commandType: command.type,493 detail: `thread ${command.threadId} changed before automatic settlement`,
494 detail: `thread ${command.threadId} changed before automatic settlement`,494 });
495 }),
496 );
497 }495 }
498 // The server owns settle eligibility. A stale command must not settle496 // The server owns settle eligibility. A stale command must not settle
499 // a thread whose session is coming alive or working.497 // a thread whose session is coming alive or working.
...@@ -645,36 +643,30 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand"...@@ -645,36 +643,30 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand"
645 // structurally just a string): NaN fails every comparison, and an643 // structurally just a string): NaN fails every comparison, and an
646 // unparseable snoozedUntil must never persist.644 // unparseable snoozedUntil must never persist.
647 if (!(Date.parse(command.snoozedUntil) > Date.parse(occurredAt))) {645 if (!(Date.parse(command.snoozedUntil) > Date.parse(occurredAt))) {
648 return yield* Effect.fail(646 return yield* new OrchestrationCommandInvariantError({
649 new OrchestrationCommandInvariantError({647 commandType: command.type,
650 commandType: command.type,648 detail: `thread ${command.threadId} snooze wake time ${command.snoozedUntil} is not in the future`,
651 detail: `thread ${command.threadId} snooze wake time ${command.snoozedUntil} is not in the future`,649 });
652 }),
653 );
654 }650 }
655 // Blocked-on-you work must not be snoozed away: a pending approval or651 // Blocked-on-you work must not be snoozed away: a pending approval or
656 // user-input request is the agent waiting on the user, and hiding it652 // user-input request is the agent waiting on the user, and hiding it
657 // defeats the request. (A running session IS snoozable — snooze only653 // defeats the request. (A running session IS snoozable — snooze only
658 // affects visibility, never the agent.)654 // affects visibility, never the agent.)
659 if (openRequests(thread).size > 0) {655 if (openRequests(thread).size > 0) {
660 return yield* Effect.fail(656 return yield* new OrchestrationCommandInvariantError({
661 new OrchestrationCommandInvariantError({657 commandType: command.type,
662 commandType: command.type,658 detail: `thread ${command.threadId} has a pending approval or user-input request and cannot be snoozed`,
663 detail: `thread ${command.threadId} has a pending approval or user-input request and cannot be snoozed`,659 });
664 }),
665 );
666 }660 }
667 // A queued turn start — a user message no turn has adopted yet — is661 // A queued turn start — a user message no turn has adopted yet — is
668 // invisible pending work: no session, no pending flags. Snoozing in662 // invisible pending work: no session, no pending flags. Snoozing in
669 // that window would hide a just-requested turn exactly the way settle663 // that window would hide a just-requested turn exactly the way settle
670 // would.664 // would.
671 if (hasQueuedTurnStartForThread(thread, occurredAt)) {665 if (hasQueuedTurnStartForThread(thread, occurredAt)) {
672 return yield* Effect.fail(666 return yield* new OrchestrationCommandInvariantError({
673 new OrchestrationCommandInvariantError({667 commandType: command.type,
674 commandType: command.type,668 detail: `thread ${command.threadId} has a queued turn start and cannot be snoozed`,
675 detail: `thread ${command.threadId} has a queued turn start and cannot be snoozed`,669 });
676 }),
677 );
678 }670 }
679 // Re-snoozing an already-snoozed thread to the SAME wake time is a671 // Re-snoozing an already-snoozed thread to the SAME wake time is a
680 // duplicate (double-click, raced clients): re-emit with the original672 // duplicate (double-click, raced clients): re-emit with the original
...@@ -836,12 +828,10 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand"...@@ -836,12 +828,10 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand"
836 // (rather than silently pinning) keeps a raced reorder-after-unpin828 // (rather than silently pinning) keeps a raced reorder-after-unpin
837 // from resurrecting a pin the user just cleared.829 // from resurrecting a pin the user just cleared.
838 if (thread.pinnedAt == null) {830 if (thread.pinnedAt == null) {
839 return yield* Effect.fail(831 return yield* new OrchestrationCommandInvariantError({
840 new OrchestrationCommandInvariantError({832 commandType: command.type,
841 commandType: command.type,833 detail: `thread ${command.threadId} is not pinned and cannot be reordered`,
842 detail: `thread ${command.threadId} is not pinned and cannot be reordered`,834 });
843 }),
844 );
845 }835 }
846 // Idempotent by re-emission (see thread.settle): a duplicate drop on836 // Idempotent by re-emission (see thread.settle): a duplicate drop on
847 // the same slot keeps the existing updatedAt so it projects as a no-op.837 // the same slot keeps the existing updatedAt so it projects as a no-op.
...@@ -953,12 +943,10 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand"...@@ -953,12 +943,10 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand"
953 if (command.linkedPullRequest != null) {943 if (command.linkedPullRequest != null) {
954 const { linkedPullRequest: linked, ...metadata } = command;944 const { linkedPullRequest: linked, ...metadata } = command;
955 const project = readModel.projects.find((project) => project.id === thread.projectId);945 const project = readModel.projects.find((project) => project.id === thread.projectId);
956 let host = project?.repositoryIdentity?.canonicalKey.split("/")[0] ?? "unknown";946 // Historical clients can send links without a parseable URL.
957 try {947 const host = URL.canParse(linked.url)
958 host = new URL(linked.url).hostname;948 ? new URL(linked.url).hostname
959 } catch {949 : (project?.repositoryIdentity?.canonicalKey.split("/")[0] ?? "unknown");
960 // Historical clients can send links without a parseable URL.
961 }
962 const hasMetadata = Object.entries(metadata).some(950 const hasMetadata = Object.entries(metadata).some(
963 ([key, value]) => !["type", "commandId", "threadId"].includes(key) && value !== undefined,951 ([key, value]) => !["type", "commandId", "threadId"].includes(key) && value !== undefined,
964 );952 );
...@@ -1872,12 +1860,10 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand"...@@ -1872,12 +1860,10 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand"
1872 sessionComingAlive ||1860 sessionComingAlive ||
1873 hasQueuedTurnStartForThread(thread, command.createdAt)1861 hasQueuedTurnStartForThread(thread, command.createdAt)
1874 ) {1862 ) {
1875 return yield* Effect.fail(1863 return yield* new OrchestrationCommandInvariantError({
1876 new OrchestrationCommandInvariantError({1864 commandType: command.type,
1877 commandType: command.type,1865 detail: `thread ${command.threadId} was re-engaged after settle; skipping session stop`,
1878 detail: `thread ${command.threadId} was re-engaged after settle; skipping session stop`,1866 });
1879 }),
1880 );
1881 }1867 }
1882 }1868 }
1883 return {1869 return {
apps/server/src/orchestration/workflowScriptQuery.ts+12-9
...@@ -32,9 +32,10 @@ export const readWorkflowScript = Effect.fn("orchestration.readWorkflowScript")(...@@ -32,9 +32,10 @@ export const readWorkflowScript = Effect.fn("orchestration.readWorkflowScript")(
32 const requested = input.scriptPath;32 const requested = input.scriptPath;
3333
34 if (!NodePath.isAbsolute(requested) || NodePath.extname(requested) !== ".js") {34 if (!NodePath.isAbsolute(requested) || NodePath.extname(requested) !== ".js") {
35 return yield* Effect.fail(35 return yield* new OrchestrationGetWorkflowScriptError({
36 new OrchestrationGetWorkflowScriptError({ reason: "invalid-path", scriptPath: requested }),36 reason: "invalid-path",
37 );37 scriptPath: requested,
38 });
38 }39 }
3940
40 const root = yield* Effect.tryPromise({41 const root = yield* Effect.tryPromise({
...@@ -60,14 +61,16 @@ export const readWorkflowScript = Effect.fn("orchestration.readWorkflowScript")(...@@ -60,14 +61,16 @@ export const readWorkflowScript = Effect.fn("orchestration.readWorkflowScript")(
60 });61 });
6162
62 if (resolved !== root && !resolved.startsWith(`${root}${NodePath.sep}`)) {63 if (resolved !== root && !resolved.startsWith(`${root}${NodePath.sep}`)) {
63 return yield* Effect.fail(64 return yield* new OrchestrationGetWorkflowScriptError({
64 new OrchestrationGetWorkflowScriptError({ reason: "outside-root", scriptPath: resolved }),65 reason: "outside-root",
65 );66 scriptPath: resolved,
67 });
66 }68 }
67 if (NodePath.extname(resolved) !== ".js") {69 if (NodePath.extname(resolved) !== ".js") {
68 return yield* Effect.fail(70 return yield* new OrchestrationGetWorkflowScriptError({
69 new OrchestrationGetWorkflowScriptError({ reason: "not-js", scriptPath: resolved }),71 reason: "not-js",
70 );72 scriptPath: resolved,
73 });
71 }74 }
7275
73 // TOCTOU-safe read (review finding): open FIRST, then verify what was76 // TOCTOU-safe read (review finding): open FIRST, then verify what was
apps/server/src/persistence/AuthPairingLinks.ts+4-4
...@@ -268,7 +268,7 @@ export const make = Effect.gen(function* () {...@@ -268,7 +268,7 @@ export const make = Effect.gen(function* () {
268 ),268 ),
269 Effect.flatMap((rowOption) =>269 Effect.flatMap((rowOption) =>
270 Option.match(rowOption, {270 Option.match(rowOption, {
271 onNone: () => Effect.succeed(Option.none()),271 onNone: () => Effect.succeedNone,
272 onSome: (row) =>272 onSome: (row) =>
273 decodeAuthPairingLinkDbRow(row).pipe(273 decodeAuthPairingLinkDbRow(row).pipe(
274 Effect.mapError((cause) =>274 Effect.mapError((cause) =>
...@@ -278,7 +278,7 @@ export const make = Effect.gen(function* () {...@@ -278,7 +278,7 @@ export const make = Effect.gen(function* () {
278 { pairingLinkId: row.id },278 { pairingLinkId: row.id },
279 ),279 ),
280 ),280 ),
281 Effect.map(Option.some),281 Effect.asSome,
282 ),282 ),
283 }),283 }),
284 ),284 ),
...@@ -329,7 +329,7 @@ export const make = Effect.gen(function* () {...@@ -329,7 +329,7 @@ export const make = Effect.gen(function* () {
329 ),329 ),
330 Effect.flatMap((rowOption) =>330 Effect.flatMap((rowOption) =>
331 Option.match(rowOption, {331 Option.match(rowOption, {
332 onNone: () => Effect.succeed(Option.none()),332 onNone: () => Effect.succeedNone,
333 onSome: (row) =>333 onSome: (row) =>
334 decodeAuthPairingLinkDbRow(row).pipe(334 decodeAuthPairingLinkDbRow(row).pipe(
335 Effect.mapError((cause) =>335 Effect.mapError((cause) =>
...@@ -339,7 +339,7 @@ export const make = Effect.gen(function* () {...@@ -339,7 +339,7 @@ export const make = Effect.gen(function* () {
339 { pairingLinkId: row.id },339 { pairingLinkId: row.id },
340 ),340 ),
341 ),341 ),
342 Effect.map(Option.some),342 Effect.asSome,
343 ),343 ),
344 }),344 }),
345 ),345 ),
apps/server/src/persistence/AuthSessions.ts+1-1
...@@ -422,7 +422,7 @@ export const make = Effect.gen(function* () {...@@ -422,7 +422,7 @@ export const make = Effect.gen(function* () {
422 ),422 ),
423 Effect.flatMap((rowOption) =>423 Effect.flatMap((rowOption) =>
424 Option.match(rowOption, {424 Option.match(rowOption, {
425 onNone: () => Effect.succeed(Option.none()),425 onNone: () => Effect.succeedNone,
426 onSome: (row) =>426 onSome: (row) =>
427 decodeAuthSessionDbRow(row).pipe(427 decodeAuthSessionDbRow(row).pipe(
428 Effect.mapError((cause) =>428 Effect.mapError((cause) =>
apps/server/src/persistence/Layers/ProjectionTurns.ts+2-3
...@@ -317,9 +317,8 @@ const makeProjectionTurnRepository = Effect.gen(function* () {...@@ -317,9 +317,8 @@ const makeProjectionTurnRepository = Effect.gen(function* () {
317 ),317 ),
318 Effect.flatMap((rowOption) =>318 Effect.flatMap((rowOption) =>
319 Option.match(rowOption, {319 Option.match(rowOption, {
320 onNone: () => Effect.succeed(Option.none()),320 onNone: () => Effect.succeedNone,
321 onSome: (row) =>321 onSome: (row) => Effect.succeedSome(row as Schema.Schema.Type<typeof ProjectionTurnById>),
322 Effect.succeed(Option.some(row as Schema.Schema.Type<typeof ProjectionTurnById>)),
323 }),322 }),
324 ),323 ),
325 );324 );
apps/server/src/persistence/ProviderSessionRuntime.ts+3-3
...@@ -398,7 +398,7 @@ export const make = Effect.gen(function* () {...@@ -398,7 +398,7 @@ export const make = Effect.gen(function* () {
398 ),398 ),
399 Effect.flatMap((runtimeRowOption) =>399 Effect.flatMap((runtimeRowOption) =>
400 Option.match(runtimeRowOption, {400 Option.match(runtimeRowOption, {
401 onNone: () => Effect.succeed(Option.none()),401 onNone: () => Effect.succeedNone,
402 onSome: (row) =>402 onSome: (row) =>
403 decodeRuntimeRow(row).pipe(403 decodeRuntimeRow(row).pipe(
404 Effect.mapError((cause) =>404 Effect.mapError((cause) =>
...@@ -408,7 +408,7 @@ export const make = Effect.gen(function* () {...@@ -408,7 +408,7 @@ export const make = Effect.gen(function* () {
408 { threadId: input.threadId },408 { threadId: input.threadId },
409 ),409 ),
410 ),410 ),
411 Effect.map((runtime) => Option.some(runtime)),411 Effect.asSome,
412 ),412 ),
413 }),413 }),
414 ),414 ),
...@@ -428,7 +428,7 @@ export const make = Effect.gen(function* () {...@@ -428,7 +428,7 @@ export const make = Effect.gen(function* () {
428 // every consumer that enumerates sessions, such as the reaper.428 // every consumer that enumerates sessions, such as the reaper.
429 Effect.forEach(rows, (row) =>429 Effect.forEach(rows, (row) =>
430 decodeRuntimeRow(row).pipe(430 decodeRuntimeRow(row).pipe(
431 Effect.map(Option.some),431 Effect.asSome,
432 Effect.catch((cause) =>432 Effect.catch((cause) =>
433 Effect.logWarning("provider.session.runtime.row-skipped", {433 Effect.logWarning("provider.session.runtime.row-skipped", {
434 threadId: row.threadId,434 threadId: row.threadId,
apps/server/src/project/AgentSessionImporter.test.ts+3-3
...@@ -232,7 +232,7 @@ it.layer(NodeServices.layer)("AgentSessionImporter", (it) => {...@@ -232,7 +232,7 @@ it.layer(NodeServices.layer)("AgentSessionImporter", (it) => {
232 upsert: (binding) => Effect.sync(() => void bindings.push(binding)),232 upsert: (binding) => Effect.sync(() => void bindings.push(binding)),
233 getProvider: () => Effect.die("unused"),233 getProvider: () => Effect.die("unused"),
234 recordImportedTranscript: () => Effect.void,234 recordImportedTranscript: () => Effect.void,
235 getBinding: () => Effect.succeed(Option.none()),235 getBinding: () => Effect.succeedNone,
236 listThreadIds: () => Effect.die("unused"),236 listThreadIds: () => Effect.die("unused"),
237 listBindings: () => Effect.die("unused"),237 listBindings: () => Effect.die("unused"),
238 });238 });
...@@ -456,7 +456,7 @@ it.layer(NodeServices.layer)("AgentSessionImporter", (it) => {...@@ -456,7 +456,7 @@ it.layer(NodeServices.layer)("AgentSessionImporter", (it) => {
456 upsert: () => Effect.die("must not replace an active binding"),456 upsert: () => Effect.die("must not replace an active binding"),
457 getProvider: () => Effect.die("unused"),457 getProvider: () => Effect.die("unused"),
458 recordImportedTranscript: () => Effect.void,458 recordImportedTranscript: () => Effect.void,
459 getBinding: () => Effect.succeed(Option.some(runningBinding)),459 getBinding: () => Effect.succeedSome(runningBinding),
460 listThreadIds: () => Effect.die("unused"),460 listThreadIds: () => Effect.die("unused"),
461 listBindings: () => Effect.die("unused"),461 listBindings: () => Effect.die("unused"),
462 });462 });
...@@ -511,7 +511,7 @@ it.layer(NodeServices.layer)("AgentSessionImporter", (it) => {...@@ -511,7 +511,7 @@ it.layer(NodeServices.layer)("AgentSessionImporter", (it) => {
511 upsert: () => Effect.die("must not bind malformed or wrong-project sessions"),511 upsert: () => Effect.die("must not bind malformed or wrong-project sessions"),
512 getProvider: () => Effect.die("unused"),512 getProvider: () => Effect.die("unused"),
513 recordImportedTranscript: () => Effect.die("unused"),513 recordImportedTranscript: () => Effect.die("unused"),
514 getBinding: () => Effect.succeed(Option.none()),514 getBinding: () => Effect.succeedNone,
515 listThreadIds: () => Effect.die("unused"),515 listThreadIds: () => Effect.die("unused"),
516 listBindings: () => Effect.die("unused"),516 listBindings: () => Effect.die("unused"),
517 });517 });
apps/server/src/project/AgentSessionScanner.ts+1-1
...@@ -662,7 +662,7 @@ export const make = Effect.gen(function* () {...@@ -662,7 +662,7 @@ export const make = Effect.gen(function* () {
662 fileSystem.readDirectory(directory).pipe(Effect.orElseSucceed((): ReadonlyArray<string> => []));662 fileSystem.readDirectory(directory).pipe(Effect.orElseSucceed((): ReadonlyArray<string> => []));
663663
664 const statOption = (target: string) =>664 const statOption = (target: string) =>
665 fileSystem.stat(target).pipe(Effect.map(Option.some), Effect.orElseSucceed(Option.none));665 fileSystem.stat(target).pipe(Effect.asSome, Effect.orElseSucceed(Option.none));
666666
667 /** Match directory aliases without assuming the host volume is case-insensitive. */667 /** Match directory aliases without assuming the host volume is case-insensitive. */
668 const directoryIdentity = Effect.fn("AgentSessionScanner.directoryIdentity")(function* (668 const directoryIdentity = Effect.fn("AgentSessionScanner.directoryIdentity")(function* (
apps/server/src/project/ProjectFaviconResolver.ts+2-2
...@@ -143,7 +143,7 @@ const optionOnNotFound = <A, R>(...@@ -143,7 +143,7 @@ const optionOnNotFound = <A, R>(
143 effect: Effect.Effect<A, PlatformError.PlatformError, R>,143 effect: Effect.Effect<A, PlatformError.PlatformError, R>,
144): Effect.Effect<Option.Option<A>, PlatformError.PlatformError, R> =>144): Effect.Effect<Option.Option<A>, PlatformError.PlatformError, R> =>
145 effect.pipe(145 effect.pipe(
146 Effect.map(Option.some),146 Effect.asSome,
147 Effect.catchTags({147 Effect.catchTags({
148 PlatformError: (error) =>148 PlatformError: (error) =>
149 error.reason._tag === "NotFound" ? Effect.succeed(Option.none<A>()) : Effect.fail(error),149 error.reason._tag === "NotFound" ? Effect.succeed(Option.none<A>()) : Effect.fail(error),
...@@ -175,7 +175,7 @@ export const make = Effect.gen(function* () {...@@ -175,7 +175,7 @@ export const make = Effect.gen(function* () {
175 relativePath,175 relativePath,
176 })176 })
177 ).pipe(177 ).pipe(
178 Effect.map(Option.some),178 Effect.asSome,
179 Effect.catchTags({179 Effect.catchTags({
180 WorkspacePathOutsideRootError: () =>180 WorkspacePathOutsideRootError: () =>
181 Effect.succeed(181 Effect.succeed(
apps/server/src/project/RepositoryIdentityResolver.ts+4-4
...@@ -141,6 +141,7 @@ export const make = Effect.fn("RepositoryIdentityResolver.make")(function* (...@@ -141,6 +141,7 @@ export const make = Effect.fn("RepositoryIdentityResolver.make")(function* (
141) {141) {
142 const processRunner = yield* ProcessRunner.ProcessRunner;142 const processRunner = yield* ProcessRunner.ProcessRunner;
143 const cacheCapacity = options.cacheCapacity ?? DEFAULT_REPOSITORY_IDENTITY_CACHE_CAPACITY;143 const cacheCapacity = options.cacheCapacity ?? DEFAULT_REPOSITORY_IDENTITY_CACHE_CAPACITY;
144 const refine = options.refine ?? Effect.succeed;
144145
145 const repositoryRootCache = yield* Cache.makeWith<string, string | null>(146 const repositoryRootCache = yield* Cache.makeWith<string, string | null>(
146 (cwd) =>147 (cwd) =>
...@@ -161,10 +162,9 @@ export const make = Effect.fn("RepositoryIdentityResolver.make")(function* (...@@ -161,10 +162,9 @@ export const make = Effect.fn("RepositoryIdentityResolver.make")(function* (
161 (cacheKey) =>162 (cacheKey) =>
162 resolveRepositoryIdentityFromCacheKey(cacheKey).pipe(163 resolveRepositoryIdentityFromCacheKey(cacheKey).pipe(
163 Effect.provideService(ProcessRunner.ProcessRunner, processRunner),164 Effect.provideService(ProcessRunner.ProcessRunner, processRunner),
164 Effect.flatMap((identity) =>165 Effect.filterOrElse(
165 identity !== null && options.refine166 (identity): identity is null => identity === null,
166 ? options.refine(identity).pipe(Effect.catch(() => Effect.succeed(identity)))167 (identity) => refine(identity).pipe(Effect.orElseSucceed(() => identity)),
167 : Effect.succeed(identity),
168 ),168 ),
169 ),169 ),
170 {170 {
apps/server/src/project/T3ProjectFileLoader.ts+2-2
...@@ -68,7 +68,7 @@ export const make = Effect.gen(function* () {...@@ -68,7 +68,7 @@ export const make = Effect.gen(function* () {
68 function* (workspaceRoot) {68 function* (workspaceRoot) {
69 const filePath = path.join(workspaceRoot, T3_PROJECT_FILE_NAME);69 const filePath = path.join(workspaceRoot, T3_PROJECT_FILE_NAME);
70 const raw = yield* fileSystem.readFileString(filePath).pipe(70 const raw = yield* fileSystem.readFileString(filePath).pipe(
71 Effect.map(Option.some),71 Effect.asSome,
72 Effect.catchTags({72 Effect.catchTags({
73 PlatformError: (error) =>73 PlatformError: (error) =>
74 error.reason._tag === "NotFound"74 error.reason._tag === "NotFound"
...@@ -87,7 +87,7 @@ export const make = Effect.gen(function* () {...@@ -87,7 +87,7 @@ export const make = Effect.gen(function* () {
87 return Option.none<T3ProjectFile>();87 return Option.none<T3ProjectFile>();
88 }88 }
89 return yield* decodeT3ProjectFileJson(raw.value).pipe(89 return yield* decodeT3ProjectFileJson(raw.value).pipe(
90 Effect.map(Option.some),90 Effect.asSome,
91 Effect.catchTags({91 Effect.catchTags({
92 SchemaError: (error) =>92 SchemaError: (error) =>
93 logT3ProjectFileLoadError(93 logT3ProjectFileLoadError(
apps/server/src/provider/AntigravityInstallation.ts+1-1
...@@ -315,7 +315,7 @@ export const makeAntigravityInstallation = Effect.fn("AntigravityInstallation.ma...@@ -315,7 +315,7 @@ export const makeAntigravityInstallation = Effect.fn("AntigravityInstallation.ma
315 );315 );
316 }316 }
317 const contents = yield* fs.readFileString(filePath);317 const contents = yield* fs.readFileString(filePath);
318 return yield* Schema.decodeUnknownEffect(Schema.fromJsonString(schema))(contents);318 return yield* Schema.decodeEffect(Schema.fromJsonString(schema))(contents);
319 });319 });
320320
321 const executableFile = Effect.fn("AntigravityInstallation.executableFile")(function* (321 const executableFile = Effect.fn("AntigravityInstallation.executableFile")(function* (
apps/server/src/provider/Drivers/AntigravitySkills.ts+1-1
...@@ -86,7 +86,7 @@ const readIfPresent = <A, R>(...@@ -86,7 +86,7 @@ const readIfPresent = <A, R>(
86 Effect.catchTags({86 Effect.catchTags({
87 PlatformError: (cause) =>87 PlatformError: (cause) =>
88 cause.reason._tag === "NotFound"88 cause.reason._tag === "NotFound"
89 ? Effect.succeed(undefined)89 ? Effect.undefined
90 : Effect.fail(90 : Effect.fail(
91 new AntigravitySkillsProbeError({ reason: "filesystem-error", path, cause }),91 new AntigravitySkillsProbeError({ reason: "filesystem-error", path, cause }),
92 ),92 ),
apps/server/src/provider/Drivers/CursorDriver.ts+11-6
...@@ -140,12 +140,17 @@ export const CursorDriver: ProviderDriver<CursorSettings, CursorDriverEnv> = {...@@ -140,12 +140,17 @@ export const CursorDriver: ProviderDriver<CursorSettings, CursorDriverEnv> = {
140 processEnv,140 processEnv,
141 modelDiscovery.discover,141 modelDiscovery.discover,
142 ).pipe(142 ).pipe(
143 Effect.flatMap((snapshot) =>143 Effect.filterOrElse(
144 effectiveConfig.enabled && snapshot.installed && snapshot.auth.status === "authenticated"144 (snapshot) =>
145 ? readCursorUsageLimits(effectiveConfig, processEnv).pipe(145 !(
146 Effect.map((usageLimits) => ({ ...snapshot, usageLimits })),146 effectiveConfig.enabled &&
147 )147 snapshot.installed &&
148 : Effect.succeed(snapshot),148 snapshot.auth.status === "authenticated"
149 ),
150 (snapshot) =>
151 readCursorUsageLimits(effectiveConfig, processEnv).pipe(
152 Effect.map((usageLimits) => ({ ...snapshot, usageLimits })),
153 ),
149 ),154 ),
150 Effect.map(stampIdentity),155 Effect.map(stampIdentity),
151 Effect.provideService(HttpClient.HttpClient, httpClient),156 Effect.provideService(HttpClient.HttpClient, httpClient),
apps/server/src/provider/Drivers/GrokDriver.ts+16-11
...@@ -131,17 +131,22 @@ export const GrokDriver: ProviderDriver<GrokSettings, GrokDriverEnv> = {...@@ -131,17 +131,22 @@ export const GrokDriver: ProviderDriver<GrokSettings, GrokDriverEnv> = {
131 const textGeneration = yield* makeGrokTextGeneration(effectiveConfig, processEnv);131 const textGeneration = yield* makeGrokTextGeneration(effectiveConfig, processEnv);
132132
133 const checkProvider = checkGrokProviderStatus(effectiveConfig, processEnv, cwd).pipe(133 const checkProvider = checkGrokProviderStatus(effectiveConfig, processEnv, cwd).pipe(
134 Effect.flatMap((snapshot) =>134 Effect.filterOrElse(
135 effectiveConfig.enabled && snapshot.installed && snapshot.auth.status === "authenticated"135 (snapshot) =>
136 ? readGrokAccount(processEnv).pipe(136 !(
137 // The email lets clients recognize one account signed in on several environments.137 effectiveConfig.enabled &&
138 Effect.map(({ email, usageLimits }) => ({138 snapshot.installed &&
139 ...snapshot,139 snapshot.auth.status === "authenticated"
140 auth: email ? { ...snapshot.auth, email } : snapshot.auth,140 ),
141 usageLimits,141 (snapshot) =>
142 })),142 readGrokAccount(processEnv).pipe(
143 )143 // The email lets clients recognize one account signed in on several environments.
144 : Effect.succeed(snapshot),144 Effect.map(({ email, usageLimits }) => ({
145 ...snapshot,
146 auth: email ? { ...snapshot.auth, email } : snapshot.auth,
147 usageLimits,
148 })),
149 ),
145 ),150 ),
146 Effect.map(stampIdentity),151 Effect.map(stampIdentity),
147 Effect.provideService(HttpClient.HttpClient, httpClient),152 Effect.provideService(HttpClient.HttpClient, httpClient),
apps/server/src/provider/Layers/AntigravityAdapter.ts+2-2
...@@ -844,7 +844,7 @@ export const makeAntigravityAdapter = Effect.fn("makeAntigravityAdapter")(functi...@@ -844,7 +844,7 @@ export const makeAntigravityAdapter = Effect.fn("makeAntigravityAdapter")(functi
844 const model = yield* applyAntigravityAcpModelSelection({844 const model = yield* applyAntigravityAcpModelSelection({
845 runtime,845 runtime,
846 model: input.modelSelection?.model,846 model: input.modelSelection?.model,
847 defaultModel: yield* options.defaultModel ?? Effect.succeed(undefined),847 defaultModel: yield* options.defaultModel ?? Effect.undefined,
848 mapError: (cause) => cause,848 mapError: (cause) => cause,
849 });849 });
850 yield* runtime.setMode(antigravityPermissionMode(input.runtimeMode));850 yield* runtime.setMode(antigravityPermissionMode(input.runtimeMode));
...@@ -1035,7 +1035,7 @@ export const makeAntigravityAdapter = Effect.fn("makeAntigravityAdapter")(functi...@@ -1035,7 +1035,7 @@ export const makeAntigravityAdapter = Effect.fn("makeAntigravityAdapter")(functi
1035 const model = resolveAntigravityModel({1035 const model = resolveAntigravityModel({
1036 configOptions,1036 configOptions,
1037 model: requestedModel,1037 model: requestedModel,
1038 defaultModel: yield* options.defaultModel ?? Effect.succeed(undefined),1038 defaultModel: yield* options.defaultModel ?? Effect.undefined,
1039 });1039 });
1040 const availableModels = antigravityModelOptions(configOptions);1040 const availableModels = antigravityModelOptions(configOptions);
1041 if (model && !availableModels.some((option) => option.value === model)) {1041 if (model && !availableModels.some((option) => option.value === model)) {
apps/server/src/provider/Layers/ClaudeAdapter.test.ts+1
...@@ -1,3 +1,4 @@...@@ -1,3 +1,4 @@
1// @effect-diagnostics abortControllerInEffect:off - Tests hand-built AbortSignals to the SDK query stub to exercise cancellation.
1import { buildRuntimeInstructions } from "../RuntimeInstructions.ts";2import { buildRuntimeInstructions } from "../RuntimeInstructions.ts";
2// @effect-diagnostics nodeBuiltinImport:off3// @effect-diagnostics nodeBuiltinImport:off
3import * as NodeFS from "node:fs";4import * as NodeFS from "node:fs";
apps/server/src/provider/Layers/ClaudeAdapter.ts+2-1
...@@ -5357,6 +5357,7 @@ export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* (...@@ -5357,6 +5357,7 @@ export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* (
5357 ) => {5357 ) => {
5358 // SDK history helpers read process.env. Isolate the provider's home instead5358 // SDK history helpers read process.env. Isolate the provider's home instead
5359 // of changing the server's environment while other providers are running.5359 // of changing the server's environment while other providers are running.
5360 // @effect-diagnostics-next-line runEffectInsideEffect:off - SDK callback runs outside the fiber; the spawn is self-contained
5360 const result = await Effect.runPromise(5361 const result = await Effect.runPromise(
5361 spawnAndCollect(5362 spawnAndCollect(
5362 process.execPath,5363 process.execPath,
...@@ -5547,7 +5548,7 @@ export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* (...@@ -5547,7 +5548,7 @@ export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* (
55475548
5548 for (const result of results) {5549 for (const result of results) {
5549 if (result._tag === "Failure") {5550 if (result._tag === "Failure") {
5550 return yield* Effect.fail(result.failure);5551 return yield* result.failure;
5551 }5552 }
5552 }5553 }
5553 });5554 });
apps/server/src/provider/Layers/CodexAdapter.test.ts+1-1
...@@ -225,7 +225,7 @@ const providerSessionDirectoryTestLayer = Layer.succeed(ProviderSessionDirectory...@@ -225,7 +225,7 @@ const providerSessionDirectoryTestLayer = Layer.succeed(ProviderSessionDirectory
225 recordImportedTranscript: () => Effect.die("unused"),225 recordImportedTranscript: () => Effect.die("unused"),
226 getProvider: () =>226 getProvider: () =>
227 Effect.die(new Error("ProviderSessionDirectory.getProvider is not used in test")),227 Effect.die(new Error("ProviderSessionDirectory.getProvider is not used in test")),
228 getBinding: () => Effect.succeed(Option.none()),228 getBinding: () => Effect.succeedNone,
229 listThreadIds: () => Effect.succeed([]),229 listThreadIds: () => Effect.succeed([]),
230 listBindings: () => Effect.succeed([]),230 listBindings: () => Effect.succeed([]),
231});231});
apps/server/src/provider/Layers/CodexSessionRuntime.ts+1-1
...@@ -1518,7 +1518,7 @@ export const makeCodexSessionRuntime = (...@@ -1518,7 +1518,7 @@ export const makeCodexSessionRuntime = (
1518 }1518 }
1519 }),1519 }),
1520 ),1520 ),
1521 Effect.catch(() => Effect.void),1521 Effect.ignore,
1522 Effect.forkIn(runtimeScope),1522 Effect.forkIn(runtimeScope),
1523 );1523 );
1524 });1524 });
apps/server/src/provider/Layers/GrokAdapter.ts+37-38
...@@ -776,46 +776,45 @@ export function makeGrokAdapter(grokSettings: GrokSettings, options?: GrokAdapte...@@ -776,46 +776,45 @@ export function makeGrokAdapter(grokSettings: GrokSettings, options?: GrokAdapte
776 );776 );
777 });777 });
778778
779 const runTurnLivenessWatchdog = Effect.fn("GrokAdapter.runTurnLivenessWatchdog")(779 const runTurnLivenessWatchdog = Effect.fn("GrokAdapter.runTurnLivenessWatchdog")(function* (
780 function* (ctx: GrokSessionContext) {780 ctx: GrokSessionContext,
781 while (true) {781 ) {
782 if (ctx.stopped) {782 while (true) {
783 return;783 if (ctx.stopped) {
784 }784 return;
785 const turnId = ctx.livenessTurnId;785 }
786 if (786 const turnId = ctx.livenessTurnId;
787 turnId === undefined ||787 if (
788 ctx.interruptedTurnIds.has(turnId) ||788 turnId === undefined ||
789 !isLiveTurn(ctx, turnId) ||789 ctx.interruptedTurnIds.has(turnId) ||
790 hasLivenessPause(ctx)790 !isLiveTurn(ctx, turnId) ||
791 ) {791 hasLivenessPause(ctx)
792 yield* Queue.take(ctx.livenessSignals);792 ) {
793 continue;793 yield* Queue.take(ctx.livenessSignals);
794 }794 continue;
795 }
795796
796 const lastActivityAtNanos = ctx.lastTurnActivityAtNanos;797 const lastActivityAtNanos = ctx.lastTurnActivityAtNanos;
797 if (lastActivityAtNanos === undefined) {798 if (lastActivityAtNanos === undefined) {
798 yield* Queue.take(ctx.livenessSignals);799 yield* Queue.take(ctx.livenessSignals);
799 continue;800 continue;
800 }801 }
801 const nowNanos = yield* Clock.monotonicTimeNanos;802 const nowNanos = yield* Clock.monotonicTimeNanos;
802 const remainingNanos = livenessTimeoutFor(ctx).nanos - (nowNanos - lastActivityAtNanos);803 const remainingNanos = livenessTimeoutFor(ctx).nanos - (nowNanos - lastActivityAtNanos);
803 if (remainingNanos <= 0n) {804 if (remainingNanos <= 0n) {
804 yield* settleStalledTurn(ctx, turnId);805 yield* settleStalledTurn(ctx, turnId);
805 continue;806 continue;
806 }807 }
807808
808 const wakeReason = yield* Effect.raceFirst(809 const wakeReason = yield* Effect.raceFirst(
809 Effect.sleep(Duration.nanos(remainingNanos)).pipe(Effect.as("timeout" as const)),810 Effect.sleep(Duration.nanos(remainingNanos)).pipe(Effect.as("timeout" as const)),
810 Queue.take(ctx.livenessSignals).pipe(Effect.as("activity" as const)),811 Queue.take(ctx.livenessSignals).pipe(Effect.as("activity" as const)),
811 );812 );
812 if (wakeReason === "timeout") {813 if (wakeReason === "timeout") {
813 yield* settleStalledTurn(ctx, turnId);814 yield* settleStalledTurn(ctx, turnId);
814 }
815 }815 }
816 },816 }
817 Effect.catch(() => Effect.void),817 }, Effect.ignore());
818 );
819818
820 const logNative = (threadId: ThreadId, method: string, payload: unknown) =>819 const logNative = (threadId: ThreadId, method: string, payload: unknown) =>
821 Effect.gen(function* () {820 Effect.gen(function* () {
...@@ -2008,7 +2007,7 @@ export function makeGrokAdapter(grokSettings: GrokSettings, options?: GrokAdapte...@@ -2008,7 +2007,7 @@ export function makeGrokAdapter(grokSettings: GrokSettings, options?: GrokAdapte
2008 errorMessage: errorMessage ?? "Grok prompt request failed.",2007 errorMessage: errorMessage ?? "Grok prompt request failed.",
2009 }),2008 }),
2010 );2009 );
2011 }).pipe(Effect.catch(() => Effect.void)),2010 }).pipe(Effect.ignore),
2012 ),2011 ),
2013 );2012 );
2014 });2013 });
apps/server/src/provider/Layers/OpenCodeAdapter.test.ts+1-1
...@@ -578,7 +578,7 @@ const providerSessionDirectoryTestLayer = Layer.succeed(ProviderSessionDirectory...@@ -578,7 +578,7 @@ const providerSessionDirectoryTestLayer = Layer.succeed(ProviderSessionDirectory
578 recordImportedTranscript: () => Effect.die("unused"),578 recordImportedTranscript: () => Effect.die("unused"),
579 getProvider: () =>579 getProvider: () =>
580 Effect.die(new Error("ProviderSessionDirectory.getProvider is not used in test")),580 Effect.die(new Error("ProviderSessionDirectory.getProvider is not used in test")),
581 getBinding: () => Effect.succeed(Option.none()),581 getBinding: () => Effect.succeedNone,
582 listThreadIds: () => Effect.succeed([]),582 listThreadIds: () => Effect.succeed([]),
583 listBindings: () => Effect.succeed([]),583 listBindings: () => Effect.succeed([]),
584});584});
apps/server/src/provider/Layers/OpenCodeAdapter.ts+8-7
...@@ -822,7 +822,7 @@ const abortOpenCodeDescendants = Effect.fn("abortOpenCodeDescendants")(function*...@@ -822,7 +822,7 @@ const abortOpenCodeDescendants = Effect.fn("abortOpenCodeDescendants")(function*
822 .pipe(822 .pipe(
823 Effect.catchIf(823 Effect.catchIf(
824 (cause) => isOpenCodeNotFound(cause),824 (cause) => isOpenCodeNotFound(cause),
825 () => Effect.void,825 () => Effect.undefined,
826 ),826 ),
827 Effect.result,827 Effect.result,
828 );828 );
...@@ -1097,7 +1097,7 @@ export function makeOpenCodeAdapter(...@@ -1097,7 +1097,7 @@ export function makeOpenCodeAdapter(
1097 readonly observedAt: string;1097 readonly observedAt: string;
1098 readonly event: Record<string, unknown>;1098 readonly event: Record<string, unknown>;
1099 },1099 },
1100 ) => writeNativeEvent(threadId, event).pipe(Effect.catchCause(() => Effect.void));1100 ) => writeNativeEvent(threadId, event).pipe(Effect.ignoreCause);
11011101
1102 const cancelIdleReconciliation = Effect.fn("cancelIdleReconciliation")(function* (1102 const cancelIdleReconciliation = Effect.fn("cancelIdleReconciliation")(function* (
1103 context: OpenCodeSessionContext,1103 context: OpenCodeSessionContext,
...@@ -1262,7 +1262,7 @@ export function makeOpenCodeAdapter(...@@ -1262,7 +1262,7 @@ export function makeOpenCodeAdapter(
1262 yield* Effect.sleep(`${delayMs} millis`);1262 yield* Effect.sleep(`${delayMs} millis`);
1263 }1263 }
1264 }).pipe(1264 }).pipe(
1265 Effect.catchCause(() => Effect.void),1265 Effect.ignoreCause,
1266 Effect.ensuring(1266 Effect.ensuring(
1267 Effect.sync(() => {1267 Effect.sync(() => {
1268 if (context.pendingIdleReconciliation === pending) {1268 if (context.pendingIdleReconciliation === pending) {
...@@ -1492,7 +1492,7 @@ export function makeOpenCodeAdapter(...@@ -1492,7 +1492,7 @@ export function makeOpenCodeAdapter(
1492 }1492 }
1493 yield* failPromptAdmissionRecovery(context, promptAdmission);1493 yield* failPromptAdmissionRecovery(context, promptAdmission);
1494 }).pipe(1494 }).pipe(
1495 Effect.catchCause(() => Effect.void),1495 Effect.ignoreCause,
1496 Effect.ensuring(1496 Effect.ensuring(
1497 Effect.sync(() => {1497 Effect.sync(() => {
1498 delete promptAdmission.recoveryFiber;1498 delete promptAdmission.recoveryFiber;
...@@ -1699,7 +1699,7 @@ export function makeOpenCodeAdapter(...@@ -1699,7 +1699,7 @@ export function makeOpenCodeAdapter(
1699 }),1699 }),
1700 Effect.catchIf(1700 Effect.catchIf(
1701 (cause) => isOpenCodeNotFound(cause),1701 (cause) => isOpenCodeNotFound(cause),
1702 () => Effect.succeed(undefined),1702 () => Effect.undefined,
1703 ),1703 ),
1704 );1704 );
1705 let sessionId: string | undefined = candidateSessionId;1705 let sessionId: string | undefined = candidateSessionId;
...@@ -2044,7 +2044,7 @@ export function makeOpenCodeAdapter(...@@ -2044,7 +2044,7 @@ export function makeOpenCodeAdapter(
2044 yield* Effect.sleep(`${delayMs} millis`);2044 yield* Effect.sleep(`${delayMs} millis`);
2045 }2045 }
2046 }).pipe(2046 }).pipe(
2047 Effect.catchCause(() => Effect.void),2047 Effect.ignoreCause,
2048 Effect.ensuring(2048 Effect.ensuring(
2049 Effect.sync(() => {2049 Effect.sync(() => {
2050 if (context.requestRelationRetries.get(requestId) === retry) {2050 if (context.requestRelationRetries.get(requestId) === retry) {
...@@ -2163,7 +2163,7 @@ export function makeOpenCodeAdapter(...@@ -2163,7 +2163,7 @@ export function makeOpenCodeAdapter(
2163 return;2163 return;
2164 }2164 }
2165 }).pipe(2165 }).pipe(
2166 Effect.catchCause(() => Effect.void),2166 Effect.ignoreCause,
2167 Effect.ensuring(2167 Effect.ensuring(
2168 Effect.sync(() => {2168 Effect.sync(() => {
2169 if (context.pendingRequestRecovery === recovery) {2169 if (context.pendingRequestRecovery === recovery) {
...@@ -2730,6 +2730,7 @@ export function makeOpenCodeAdapter(...@@ -2730,6 +2730,7 @@ export function makeOpenCodeAdapter(
2730 // the scope closes (explicit stop, unexpected exit, or layer2730 // the scope closes (explicit stop, unexpected exit, or layer
2731 // shutdown) and cancels the in-flight `event.subscribe` fetch so2731 // shutdown) and cancels the in-flight `event.subscribe` fetch so
2732 // the async iterable unwinds cleanly.2732 // the async iterable unwinds cleanly.
2733 // @effect-diagnostics-next-line abortControllerInEffect:off - aborted by a scope finalizer to cancel the SDK's event.subscribe fetch
2733 const eventsAbortController = new AbortController();2734 const eventsAbortController = new AbortController();
2734 let lastStreamError: unknown;2735 let lastStreamError: unknown;
2735 let warnedAboutDisconnect = false;2736 let warnedAboutDisconnect = false;
apps/server/src/provider/Layers/ProviderService.test.ts+5-7
...@@ -4894,13 +4894,11 @@ const listThreadIds = vi.fn(() =>...@@ -4894,13 +4894,11 @@ const listThreadIds = vi.fn(() =>
4894 Effect.succeed([activeSessionThreadId, historicalSessionThreadId]),4894 Effect.succeed([activeSessionThreadId, historicalSessionThreadId]),
4895);4895);
4896const getBinding = vi.fn((threadId: ThreadId) =>4896const getBinding = vi.fn((threadId: ThreadId) =>
4897 Effect.succeed(4897 Effect.succeedSome({
4898 Option.some({4898 threadId,
4899 threadId,4899 provider: CODEX_DRIVER,
4900 provider: CODEX_DRIVER,4900 providerInstanceId: codexInstanceId,
4901 providerInstanceId: codexInstanceId,4901 }),
4902 }),
4903 ),
4904);4902);
4905const boundedListing = makeProviderServiceLayer({4903const boundedListing = makeProviderServiceLayer({
4906 directory: {4904 directory: {
apps/server/src/provider/Layers/ProviderService.ts+1-1
...@@ -2298,7 +2298,7 @@ const makeProviderService = Effect.fn("makeProviderService")(function* (...@@ -2298,7 +2298,7 @@ const makeProviderService = Effect.fn("makeProviderService")(function* (
2298 // Continuation is project-scopable, so decide it per session's project;2298 // Continuation is project-scopable, so decide it per session's project;
2299 // without orchestration the environment value is all there is.2299 // without orchestration the environment value is all there is.
2300 const stopSettings = yield* serverSettings.getSettings.pipe(2300 const stopSettings = yield* serverSettings.getSettings.pipe(
2301 Effect.map(Option.some),2301 Effect.asSome,
2302 Effect.orElseSucceed(() => Option.none<ServerSettingsValue>()),2302 Effect.orElseSucceed(() => Option.none<ServerSettingsValue>()),
2303 );2303 );
2304 const continueAfterRestartFor = Effect.fn("continueAfterRestartFor")(function* (2304 const continueAfterRestartFor = Effect.fn("continueAfterRestartFor")(function* (
apps/server/src/provider/Layers/ProviderSessionDirectory.ts+1-3
...@@ -93,9 +93,7 @@ const makeProviderSessionDirectory = Effect.gen(function* () {...@@ -93,9 +93,7 @@ const makeProviderSessionDirectory = Effect.gen(function* () {
93 Option.match(runtime, {93 Option.match(runtime, {
94 onNone: () => Effect.succeed(Option.none<ProviderRuntimeBinding>()),94 onNone: () => Effect.succeed(Option.none<ProviderRuntimeBinding>()),
95 onSome: (value) =>95 onSome: (value) =>
96 toRuntimeBinding(value, "ProviderSessionDirectory.getBinding").pipe(96 toRuntimeBinding(value, "ProviderSessionDirectory.getBinding").pipe(Effect.asSome),
97 Effect.map((binding) => Option.some(binding)),
98 ),
99 }),97 }),
100 ),98 ),
101 );99 );
apps/server/src/provider/Layers/claudeResetCredits.ts+1-1
...@@ -186,7 +186,7 @@ export const readClaudeResetCredits = Effect.fn("readClaudeResetCredits")(...@@ -186,7 +186,7 @@ export const readClaudeResetCredits = Effect.fn("readClaudeResetCredits")(
186 );186 );
187 },187 },
188 Effect.timeout("10 seconds"),188 Effect.timeout("10 seconds"),
189 Effect.catch(() => Effect.succeed(undefined)),189 Effect.orElseSucceed(() => undefined),
190);190);
191191
192/** The CLI keeps the account record beside its settings, or in the home directory by default. */192/** The CLI keeps the account record beside its settings, or in the home directory by default. */
apps/server/src/provider/Layers/cursorUsageLimits.ts+6-8
...@@ -127,14 +127,12 @@ export const readCursorUsageLimits = Effect.fn("readCursorUsageLimits")(function...@@ -127,14 +127,12 @@ export const readCursorUsageLimits = Effect.fn("readCursorUsageLimits")(function
127 return cursorUsageResponseToLimits(body, checkedAt);127 return cursorUsageResponseToLimits(body, checkedAt);
128 }).pipe(128 }).pipe(
129 Effect.timeout("10 seconds"),129 Effect.timeout("10 seconds"),
130 Effect.catch(() =>130 Effect.orElseSucceed(() =>
131 Effect.succeed(131 makeUnavailableUsageLimits({
132 makeUnavailableUsageLimits({132 checkedAt,
133 checkedAt,133 reason: "probeFailed",
134 reason: "probeFailed",134 message: "Cursor could not read usage limits.",
135 message: "Cursor could not read usage limits.",135 }),
136 }),
137 ),
138 ),136 ),
139 );137 );
140});138});
apps/server/src/provider/Layers/grokUsageLimits.ts+1-1
...@@ -169,7 +169,7 @@ export const readGrokAccount = Effect.fn("readGrokAccount")(function* (...@@ -169,7 +169,7 @@ export const readGrokAccount = Effect.fn("readGrokAccount")(function* (
169 return grokUsageResponseToLimits(body, checkedAt);169 return grokUsageResponseToLimits(body, checkedAt);
170 }).pipe(170 }).pipe(
171 Effect.timeout("10 seconds"),171 Effect.timeout("10 seconds"),
172 Effect.catch(() => Effect.succeed(probeFailed)),172 Effect.orElseSucceed(() => probeFailed),
173 );173 );
174 return { email, usageLimits };174 return { email, usageLimits };
175});175});
apps/server/src/provider/ModelManifest.ts+1-1
...@@ -408,7 +408,7 @@ export const make = Effect.gen(function* () {...@@ -408,7 +408,7 @@ export const make = Effect.gen(function* () {
408 fetchedAtMs = now;408 fetchedAtMs = now;
409 yield* encodeManifestCache({ fetchedAtMs: now, manifest: fetched }).pipe(409 yield* encodeManifestCache({ fetchedAtMs: now, manifest: fetched }).pipe(
410 Effect.flatMap((serialized) => fileSystem.writeFileString(cachePath, serialized)),410 Effect.flatMap((serialized) => fileSystem.writeFileString(cachePath, serialized)),
411 Effect.catchCause(() => Effect.void),411 Effect.ignoreCause,
412 );412 );
413 return manifest;413 return manifest;
414 });414 });
apps/server/src/provider/acp/AcpSessionRuntime.ts+27-30
...@@ -780,17 +780,16 @@ export const make = (...@@ -780,17 +780,16 @@ export const make = (
780 resumePayload,780 resumePayload,
781 acp.agent.resumeSession(resumePayload).pipe(781 acp.agent.resumeSession(resumePayload).pipe(
782 Effect.timeoutOption(options.sessionLoadTimeout ?? defaultSessionLoadTimeout),782 Effect.timeoutOption(options.sessionLoadTimeout ?? defaultSessionLoadTimeout),
783 Effect.flatMap((result) =>783 Effect.flatMap(
784 Option.isSome(result)784 Effect.fromOption(
785 ? Effect.succeed(result.value)785 () =>
786 : Effect.fail(786 new EffectAcpErrors.AcpTransportError({
787 new EffectAcpErrors.AcpTransportError({787 operation: "call-rpc",
788 operation: "call-rpc",788 method: "session/resume",
789 method: "session/resume",789 detail: "session/resume timed out waiting for the agent response.",
790 detail: "session/resume timed out waiting for the agent response.",790 cause: undefined,
791 cause: undefined,791 }),
792 }),792 ),
793 ),
794 ),793 ),
795 ),794 ),
796 );795 );
...@@ -834,19 +833,16 @@ export const make = (...@@ -834,19 +833,16 @@ export const make = (
834 ).pipe(833 ).pipe(
835 Effect.ensuring(Fiber.interrupt(idleFiber).pipe(Effect.ignore)),834 Effect.ensuring(Fiber.interrupt(idleFiber).pipe(Effect.ignore)),
836 Effect.timeoutOption(sessionLoadTimeout),835 Effect.timeoutOption(sessionLoadTimeout),
837 Effect.flatMap((result) =>836 Effect.flatMap(
838 Option.match(result, {837 Effect.fromOption(
839 onNone: () =>838 () =>
840 Effect.fail(839 new EffectAcpErrors.AcpTransportError({
841 new EffectAcpErrors.AcpTransportError({840 operation: "call-rpc",
842 operation: "call-rpc",841 method: "session/load",
843 method: "session/load",842 detail: "session/load timed out waiting for RPC response or replay idle gap",
844 detail: "session/load timed out waiting for RPC response or replay idle gap",843 cause: undefined,
845 cause: undefined,844 }),
846 }),845 ),
847 ),
848 onSome: Effect.succeed,
849 }),
850 ),846 ),
851 Effect.tap((result) =>847 Effect.tap((result) =>
852 logRequest({848 logRequest({
...@@ -1058,12 +1054,13 @@ export const make = (...@@ -1058,12 +1054,13 @@ export const make = (
1058 ),1054 ),
1059 (activePrompt) =>1055 (activePrompt) =>
1060 Fiber.join(activePrompt.fiber).pipe(1056 Fiber.join(activePrompt.fiber).pipe(
1061 Effect.catchCause((cause) =>1057 Effect.catchCauseIf(
1062 options.cancelBehavior !== "wait-for-prompt" && Cause.hasInterruptsOnly(cause)1058 (cause) =>
1063 ? Effect.succeed({1059 options.cancelBehavior !== "wait-for-prompt" && Cause.hasInterruptsOnly(cause),
1064 stopReason: "cancelled",1060 () =>
1065 } satisfies EffectAcpSchema.PromptResponse)1061 Effect.succeed({
1066 : Effect.failCause(cause),1062 stopReason: "cancelled",
1063 } satisfies EffectAcpSchema.PromptResponse),
1067 ),1064 ),
1068 Effect.tap(() =>1065 Effect.tap(() =>
1069 closeActiveAssistantSegment({ queue: eventQueue, assistantSegmentRef }),1066 closeActiveAssistantSegment({ queue: eventQueue, assistantSegmentRef }),
apps/server/src/provider/antigravityAuthSupport.ts+1-3
...@@ -277,9 +277,7 @@ const linkAntigravityUserSkills = Effect.fn("linkAntigravityUserSkills")(functio...@@ -277,9 +277,7 @@ const linkAntigravityUserSkills = Effect.fn("linkAntigravityUserSkills")(functio
277 yield* Effect.gen(function* () {277 yield* Effect.gen(function* () {
278 const existing = yield* fs.readLink(link).pipe(278 const existing = yield* fs.readLink(link).pipe(
279 Effect.map((value): string | undefined => path.resolve(path.dirname(link), value)),279 Effect.map((value): string | undefined => path.resolve(path.dirname(link), value)),
280 Effect.catch((error) =>280 Effect.catchReason("PlatformError", "NotFound", () => Effect.undefined),
281 error.reason._tag === "NotFound" ? Effect.succeed(undefined) : Effect.fail(error),
282 ),
283 );281 );
284 if (existing === target) return;282 if (existing === target) return;
285 if (existing !== undefined) {283 if (existing !== undefined) {
apps/server/src/provider/makeManagedServerProvider.ts+1
...@@ -252,6 +252,7 @@ export const makeManagedServerProvider = Effect.fn("makeManagedServerProvider")(...@@ -252,6 +252,7 @@ export const makeManagedServerProvider = Effect.fn("makeManagedServerProvider")(
252 yield* Effect.forever(252 yield* Effect.forever(
253 getRefreshInterval.pipe(253 getRefreshInterval.pipe(
254 Effect.flatMap((refreshInterval) =>254 Effect.flatMap((refreshInterval) =>
255 // @effect-diagnostics-next-line raceFirstWithSleepToTimeout:off - races the interval against a settings-change signal, not a timeout
255 Effect.raceFirst(256 Effect.raceFirst(
256 Effect.sleep(257 Effect.sleep(
257 Duration.toMillis(Duration.fromInputUnsafe(refreshInterval)) <= 0258 Duration.toMillis(Duration.fromInputUnsafe(refreshInterval)) <= 0
apps/server/src/provider/providerInstallation.ts+1-1
...@@ -118,7 +118,7 @@ export const makeProviderInstallation = Effect.fn("makeProviderInstallation")(fu...@@ -118,7 +118,7 @@ export const makeProviderInstallation = Effect.fn("makeProviderInstallation")(fu
118 env: mergeProviderInstanceEnvironment(entry.environment),118 env: mergeProviderInstanceEnvironment(entry.environment),
119 }).pipe(119 }).pipe(
120 Effect.map((resolved) => [binaryPath, resolved]),120 Effect.map((resolved) => [binaryPath, resolved]),
121 Effect.catch(() => Effect.succeed([binaryPath])),121 Effect.orElseSucceed(() => [binaryPath]),
122 );122 );
123 });123 });
124 yield* installation124 yield* installation
apps/server/src/pullRequest/GitHubPullRequestCli.ts+26-27
...@@ -1595,31 +1595,30 @@ export const make = Effect.gen(function* () {...@@ -1595,31 +1595,30 @@ export const make = Effect.gen(function* () {
1595 decode: decodePullRequestCoreJson,1595 decode: decodePullRequestCoreJson,
1596 }),1596 }),
1597 ),1597 ),
1598 ).pipe(1598 // gh already pages check contexts. Keep its complete, deduplicated result for
1599 Effect.flatMap((core) => {1599 // large check suites instead of letting the first 100 checks imply success.
1600 if (!core.checksTruncated) return Effect.succeed(core);1600 Effect.filterOrElse(
1601 // gh already pages check contexts. Keep its complete, deduplicated result for1601 (core) => !core.checksTruncated,
1602 // large check suites instead of letting the first 100 checks imply success.1602 (core) =>
1603 return readLegacyDetail(input).pipe(1603 readLegacyDetail(input).pipe(
1604 Effect.flatMap((detail) =>1604 Effect.filterOrFail(
1605 detail.headSha !== core.headSha1605 (detail) => detail.headSha === core.headSha,
1606 ? Effect.fail(1606 () =>
1607 new GitHubPullRequestReadError({1607 new GitHubPullRequestReadError({
1608 command: "gh",1608 command: "gh",
1609 cwd: input.cwd,1609 cwd: input.cwd,
1610 operation: "getPullRequestDetail",1610 operation: "getPullRequestDetail",
1611 cause: new Error("Pull request head changed while reading checks."),1611 cause: new Error("Pull request head changed while reading checks."),
1612 }),
1613 )
1614 : Effect.succeed({
1615 ...core,
1616 checks: detail.checks,
1617 checksState: detail.checksState,
1618 checksTruncated: false,
1619 }),1612 }),
1613 ),
1614 Effect.map((detail) => ({
1615 ...core,
1616 checks: detail.checks,
1617 checksState: detail.checksState,
1618 checksTruncated: false,
1619 })),
1620 ),1620 ),
1621 );1621 ),
1622 }),
1623 );1622 );
1624 };1623 };
16251624
...@@ -1967,10 +1966,9 @@ export const make = Effect.gen(function* () {...@@ -1967,10 +1966,9 @@ export const make = Effect.gen(function* () {
1967 // the fallback out: an empty answer under one is already the answer.1966 // the fallback out: an empty answer under one is already the answer.
1968 const hasQuery = (input.query?.trim().length ?? 0) > 0;1967 const hasQuery = (input.query?.trim().length ?? 0) > 0;
1969 return read(true).pipe(1968 return read(true).pipe(
1970 Effect.flatMap((batch) =>1969 Effect.filterOrElse(
1971 batch.items.length === 0 && input.cursor === undefined && !hasQuery1970 (batch) => batch.items.length > 0 || input.cursor !== undefined || hasQuery,
1972 ? read(false)1971 () => read(false),
1973 : Effect.succeed(batch),
1974 ),1972 ),
1975 Effect.flatMap((batch) => {1973 Effect.flatMap((batch) => {
1976 // Match the search query's host support, and enrich only rows that survived paging.1974 // Match the search query's host support, and enrich only rows that survived paging.
...@@ -2131,6 +2129,7 @@ export const make = Effect.gen(function* () {...@@ -2131,6 +2129,7 @@ export const make = Effect.gen(function* () {
2131 }),2129 }),
2132 );2130 );
2133 }),2131 }),
2132 // @effect-diagnostics-next-line flatMapConditionalToFilterOrFail:off - the fallback needs a non-null stack, which a predicate that also reads includeDetails cannot refine.
2134 Effect.flatMap((stack) => {2133 Effect.flatMap((stack) => {
2135 if (!input.includeDetails || stack === null) return Effect.succeed(stack);2134 if (!input.includeDetails || stack === null) return Effect.succeed(stack);
2136 return github2135 return github
apps/server/src/pullRequest/GitLabPullRequestCli.ts+1-1
...@@ -1250,7 +1250,7 @@ export const make = Effect.gen(function* () {...@@ -1250,7 +1250,7 @@ export const make = Effect.gen(function* () {
1250 getMergeRequestDiffFileContents: (input) =>1250 getMergeRequestDiffFileContents: (input) =>
1251 Effect.gen(function* () {1251 Effect.gen(function* () {
1252 if (input.commit !== undefined && !isCommitSha(input.commit)) {1252 if (input.commit !== undefined && !isCommitSha(input.commit)) {
1253 return yield* Effect.fail(new GitLabDiffCommitError({ command: "glab", cwd: input.cwd }));1253 return yield* new GitLabDiffCommitError({ command: "glab", cwd: input.cwd });
1254 }1254 }
1255 const refs = yield* input.commit === undefined1255 const refs = yield* input.commit === undefined
1256 ? getDiffRefs(input)1256 ? getDiffRefs(input)
apps/server/src/pullRequest/PullRequestReadCache.ts+1-1
...@@ -77,7 +77,7 @@ export const make = Effect.gen(function* () {...@@ -77,7 +77,7 @@ export const make = Effect.gen(function* () {
77 () =>77 () =>
78 backing78 backing
79 .get("revisions")79 .get("revisions")
80 .pipe(Effect.flatMap((raw) => Schema.decodeUnknownEffect(revisionCodec)(raw ?? "{}"))),80 .pipe(Effect.flatMap((raw) => Schema.decodeEffect(revisionCodec)(raw ?? "{}"))),
81 {81 {
82 capacity: 1,82 capacity: 1,
83 timeToLive: (exit) => (Exit.isSuccess(exit) ? Duration.infinity : Duration.zero),83 timeToLive: (exit) => (Exit.isSuccess(exit) ? Duration.infinity : Duration.zero),
apps/server/src/pullRequest/PullRequestService.test.ts+5-3
...@@ -3251,13 +3251,13 @@ it.effect("shares one cold viewer lookup across distinct concurrent lists", () =...@@ -3251,13 +3251,13 @@ it.effect("shares one cold viewer lookup across distinct concurrent lists", () =
3251 ],3251 ],
3252 });3252 });
32533253
3254 yield* Effect.all(3254 yield* Effect.forEach(
3255 ["all", "authored", "reviewing"].map((involvement) =>3255 ["all", "authored", "reviewing"],
3256 (involvement) =>
3256 service.list({3257 service.list({
3257 state: "open",3258 state: "open",
3258 involvement: involvement as "all" | "authored" | "reviewing",3259 involvement: involvement as "all" | "authored" | "reviewing",
3259 }),3260 }),
3260 ),
3261 { concurrency: "unbounded" },3261 { concurrency: "unbounded" },
3262 );3262 );
32633263
...@@ -4328,6 +4328,7 @@ it.effect("keeps routed reads separate when the GitHub account changes", () =>...@@ -4328,6 +4328,7 @@ it.effect("keeps routed reads separate when the GitHub account changes", () =>
4328 ],4328 ],
4329 });4329 });
4330 const readOperation = (input: Parameters<typeof service.diff>[0]) =>4330 const readOperation = (input: Parameters<typeof service.diff>[0]) =>
4331 // @effect-diagnostics-next-line unnecessaryEffectGen:off - the generator unifies the per-operation union of Effect types, which Effect.asVoid cannot infer through.
4331 Effect.gen(function* () {4332 Effect.gen(function* () {
4332 yield* service[operation](input);4333 yield* service[operation](input);
4333 });4334 });
...@@ -4391,6 +4392,7 @@ it.effect("isolates routed caches for two credentials belonging to the same acco...@@ -4391,6 +4392,7 @@ it.effect("isolates routed caches for two credentials belonging to the same acco
4391 ],4392 ],
4392 });4393 });
4393 const readOperation = (input: Parameters<typeof service.diff>[0]) =>4394 const readOperation = (input: Parameters<typeof service.diff>[0]) =>
4395 // @effect-diagnostics-next-line unnecessaryEffectGen:off - the generator unifies the per-operation union of Effect types, which Effect.asVoid cannot infer through.
4394 Effect.gen(function* () {4396 Effect.gen(function* () {
4395 yield* service[operation](input);4397 yield* service[operation](input);
4396 });4398 });
apps/server/src/pullRequest/PullRequestService.ts+1-1
...@@ -2751,7 +2751,7 @@ export const make = Effect.gen(function* () {...@@ -2751,7 +2751,7 @@ export const make = Effect.gen(function* () {
2751 `project:${input.projectId}`,2751 `project:${input.projectId}`,
2752 refScope(input),2752 refScope(input),
2753 ]);2753 ]);
2754 const decoded = yield* Schema.decodeUnknownEffect(codec)(payload).pipe(Effect.option);2754 const decoded = yield* Schema.decodeEffect(codec)(payload).pipe(Effect.option);
2755 return Option.isSome(decoded) ? decoded.value : yield* lookup;2755 return Option.isSome(decoded) ? decoded.value : yield* lookup;
2756 });2756 });
2757 const summaryCodec = Schema.fromJsonString(PullRequestSummary);2757 const summaryCodec = Schema.fromJsonString(PullRequestSummary);
apps/server/src/relay/AgentAwarenessRelay.test.ts+4-4
...@@ -576,7 +576,7 @@ describe.sequential("signRelayAgentActivityPublishProof", () => {...@@ -576,7 +576,7 @@ describe.sequential("signRelayAgentActivityPublishProof", () => {
576 yield* Deferred.await(releaseThreadShell);576 yield* Deferred.await(releaseThreadShell);
577 return Option.some(thread);577 return Option.some(thread);
578 }),578 }),
579 getProjectShellById: () => Effect.succeed(Option.some(project)),579 getProjectShellById: () => Effect.succeedSome(project),
580 } as unknown as ProjectionSnapshotQueryShape;580 } as unknown as ProjectionSnapshotQueryShape;
581581
582 const descriptor = {582 const descriptor = {
...@@ -793,8 +793,8 @@ describe.sequential("signRelayAgentActivityPublishProof", () => {...@@ -793,8 +793,8 @@ describe.sequential("signRelayAgentActivityPublishProof", () => {
793 threads: [thread],793 threads: [thread],
794 updatedAt: now,794 updatedAt: now,
795 } satisfies OrchestrationShellSnapshot),795 } satisfies OrchestrationShellSnapshot),
796 getThreadShellById: () => Effect.succeed(Option.some(thread)),796 getThreadShellById: () => Effect.succeedSome(thread),
797 getProjectShellById: () => Effect.succeed(Option.some(project)),797 getProjectShellById: () => Effect.succeedSome(project),
798 } as unknown as ProjectionSnapshotQueryShape),798 } as unknown as ProjectionSnapshotQueryShape),
799 );799 );
800800
...@@ -915,7 +915,7 @@ describe.sequential("signRelayAgentActivityPublishProof", () => {...@@ -915,7 +915,7 @@ describe.sequential("signRelayAgentActivityPublishProof", () => {
915 Layer.succeed(OrchestrationEngineService, {} as OrchestrationEngineShape),915 Layer.succeed(OrchestrationEngineService, {} as OrchestrationEngineShape),
916 Layer.succeed(ProjectionSnapshotQuery, {916 Layer.succeed(ProjectionSnapshotQuery, {
917 getThreadShellById: () => Effect.sync(() => Option.fromNullishOr(currentThread)),917 getThreadShellById: () => Effect.sync(() => Option.fromNullishOr(currentThread)),
918 getProjectShellById: () => Effect.succeed(Option.some(project)),918 getProjectShellById: () => Effect.succeedSome(project),
919 } as unknown as ProjectionSnapshotQueryShape),919 } as unknown as ProjectionSnapshotQueryShape),
920 );920 );
921921
apps/server/src/resourceTelemetry/HostResources.ts+2-2
...@@ -60,7 +60,7 @@ export const make = Effect.fn("makeHostResources")(function* () {...@@ -60,7 +60,7 @@ export const make = Effect.fn("makeHostResources")(function* () {
60 if (platform === "linux") {60 if (platform === "linux") {
61 const meminfo = yield* fs61 const meminfo = yield* fs
62 .readFileString("/proc/meminfo")62 .readFileString("/proc/meminfo")
63 .pipe(Effect.catch(() => Effect.succeed("")));63 .pipe(Effect.orElseSucceed(() => ""));
64 const available = /^MemAvailable:\s+(\d+)\s+kB$/m.exec(meminfo)?.[1];64 const available = /^MemAvailable:\s+(\d+)\s+kB$/m.exec(meminfo)?.[1];
65 if (available) availableMemoryBytes = Number(available) * 1024;65 if (available) availableMemoryBytes = Number(available) * 1024;
66 } else if (platform === "darwin") {66 } else if (platform === "darwin") {
...@@ -68,7 +68,7 @@ export const make = Effect.fn("makeHostResources")(function* () {...@@ -68,7 +68,7 @@ export const make = Effect.fn("makeHostResources")(function* () {
68 .string(ChildProcess.make("/usr/bin/vm_stat", [], { stdin: "ignore", stderr: "ignore" }))68 .string(ChildProcess.make("/usr/bin/vm_stat", [], { stdin: "ignore", stderr: "ignore" }))
69 .pipe(69 .pipe(
70 Effect.timeout("1 second"),70 Effect.timeout("1 second"),
71 Effect.catch(() => Effect.succeed("")),71 Effect.orElseSucceed(() => ""),
72 );72 );
73 availableMemoryBytes = darwinAvailableMemory(output) ?? availableMemoryBytes;73 availableMemoryBytes = darwinAvailableMemory(output) ?? availableMemoryBytes;
74 }74 }
apps/server/src/server.test.ts+111-129
...@@ -655,25 +655,25 @@ const buildAppUnderTest = (options?: {...@@ -655,25 +655,25 @@ const buildAppUnderTest = (options?: {
655 get: () => Effect.succeed(defaultVcsDriver),655 get: () => Effect.succeed(defaultVcsDriver),
656 detect: (input) =>656 detect: (input) =>
657 defaultVcsDriver.detectRepository(input.cwd).pipe(657 defaultVcsDriver.detectRepository(input.cwd).pipe(
658 Effect.flatMap((repository) =>658 Effect.filterOrElse(
659 repository659 (repository) => repository !== null,
660 ? Effect.succeed(repository)660 () =>
661 : defaultVcsDriver.isInsideWorkTree(input.cwd).pipe(661 defaultVcsDriver.isInsideWorkTree(input.cwd).pipe(
662 Effect.map((isInsideWorkTree) =>662 Effect.map((isInsideWorkTree) =>
663 isInsideWorkTree663 isInsideWorkTree
664 ? {664 ? {
665 kind: "git" as const,665 kind: "git" as const,
666 rootPath: input.cwd,666 rootPath: input.cwd,
667 metadataPath: null,667 metadataPath: null,
668 freshness: {668 freshness: {
669 source: "live-local" as const,669 source: "live-local" as const,
670 observedAt: TEST_EPOCH,670 observedAt: TEST_EPOCH,
671 expiresAt: Option.none(),671 expiresAt: Option.none(),
672 },672 },
673 }673 }
674 : null,674 : null,
675 ),
676 ),675 ),
676 ),
677 ),677 ),
678 Effect.map((repository) =>678 Effect.map((repository) =>
679 repository679 repository
...@@ -822,7 +822,7 @@ const buildAppUnderTest = (options?: {...@@ -822,7 +822,7 @@ const buildAppUnderTest = (options?: {
822 ...options?.layers?.providerAuth,822 ...options?.layers?.providerAuth,
823 }),823 }),
824 Layer.mock(ProviderInstanceRegistry)({824 Layer.mock(ProviderInstanceRegistry)({
825 getInstance: () => Effect.succeed(undefined),825 getInstance: () => Effect.undefined,
826 listInstances: Effect.succeed([]),826 listInstances: Effect.succeed([]),
827 ...options?.layers?.providerInstanceRegistry,827 ...options?.layers?.providerInstanceRegistry,
828 }),828 }),
...@@ -832,7 +832,7 @@ const buildAppUnderTest = (options?: {...@@ -832,7 +832,7 @@ const buildAppUnderTest = (options?: {
832 }),832 }),
833 Layer.mock(ProviderSessionDirectory.ProviderSessionDirectory)({833 Layer.mock(ProviderSessionDirectory.ProviderSessionDirectory)({
834 upsert: () => Effect.void,834 upsert: () => Effect.void,
835 getBinding: () => Effect.succeed(Option.none()),835 getBinding: () => Effect.succeedNone,
836 listThreadIds: () => Effect.succeed([]),836 listThreadIds: () => Effect.succeed([]),
837 listBindings: () => Effect.succeed([]),837 listBindings: () => Effect.succeed([]),
838 ...options?.layers?.providerSessionDirectory,838 ...options?.layers?.providerSessionDirectory,
...@@ -858,7 +858,7 @@ const buildAppUnderTest = (options?: {...@@ -858,7 +858,7 @@ const buildAppUnderTest = (options?: {
858 Layer.mergeAll(858 Layer.mergeAll(
859 Layer.mock(ExternalLauncher.ExternalLauncher)({859 Layer.mock(ExternalLauncher.ExternalLauncher)({
860 resolveAvailableEditors: () => Effect.succeed([]),860 resolveAvailableEditors: () => Effect.succeed([]),
861 resolveFileManagerRevealKind: () => Effect.sync((): undefined => undefined),861 resolveFileManagerRevealKind: () => Effect.undefined,
862 ...options?.layers?.externalLauncher,862 ...options?.layers?.externalLauncher,
863 }),863 }),
864 Layer.mock(RemoteOpenTargets.RemoteOpenTargets)({864 Layer.mock(RemoteOpenTargets.RemoteOpenTargets)({
...@@ -1034,20 +1034,20 @@ const buildAppUnderTest = (options?: {...@@ -1034,20 +1034,20 @@ const buildAppUnderTest = (options?: {
1034 }),1034 }),
1035 searchThreads: () => Effect.succeed({ matches: [] }),1035 searchThreads: () => Effect.succeed({ matches: [] }),
1036 getSnapshotSequence: () => Effect.succeed({ snapshotSequence: 0 }),1036 getSnapshotSequence: () => Effect.succeed({ snapshotSequence: 0 }),
1037 getProjectShellById: () => Effect.succeed(Option.none()),1037 getProjectShellById: () => Effect.succeedNone,
1038 getThreadShellById: () => Effect.succeed(Option.none()),1038 getThreadShellById: () => Effect.succeedNone,
1039 getThreadDetailById: () => Effect.succeed(Option.none()),1039 getThreadDetailById: () => Effect.succeedNone,
1040 getThreadDetailSnapshot: () => Effect.succeed(Option.none()),1040 getThreadDetailSnapshot: () => Effect.succeedNone,
1041 getCounts: () => Effect.succeed({ projectCount: 0, threadCount: 0 }),1041 getCounts: () => Effect.succeed({ projectCount: 0, threadCount: 0 }),
1042 getEventReplayStats: ({ fromSequenceExclusive, toSequenceInclusive }) =>1042 getEventReplayStats: ({ fromSequenceExclusive, toSequenceInclusive }) =>
1043 Effect.succeed({1043 Effect.succeed({
1044 eventCount: Math.max(0, toSequenceInclusive - fromSequenceExclusive),1044 eventCount: Math.max(0, toSequenceInclusive - fromSequenceExclusive),
1045 payloadBytes: 0,1045 payloadBytes: 0,
1046 }),1046 }),
1047 getActiveProjectByWorkspaceRoot: () => Effect.succeed(Option.none()),1047 getActiveProjectByWorkspaceRoot: () => Effect.succeedNone,
1048 getFirstActiveThreadIdByProjectId: () => Effect.succeed(Option.none()),1048 getFirstActiveThreadIdByProjectId: () => Effect.succeedNone,
1049 getImportedAgentSessionSources: () => Effect.succeed([]),1049 getImportedAgentSessionSources: () => Effect.succeed([]),
1050 getThreadCheckpointContext: () => Effect.succeed(Option.none()),1050 getThreadCheckpointContext: () => Effect.succeedNone,
1051 ...options?.layers?.projectionSnapshotQuery,1051 ...options?.layers?.projectionSnapshotQuery,
1052 }),1052 }),
1053 ),1053 ),
...@@ -1201,7 +1201,7 @@ const buildAppUnderTest = (options?: {...@@ -1201,7 +1201,7 @@ const buildAppUnderTest = (options?: {
1201 Layer.provide(1201 Layer.provide(
1202 Layer.mock(CloudCliTokenManager.CloudCliTokenManager)({1202 Layer.mock(CloudCliTokenManager.CloudCliTokenManager)({
1203 get: Effect.die(new Error("Unexpected T3 Connect CLI authorization request.")),1203 get: Effect.die(new Error("Unexpected T3 Connect CLI authorization request.")),
1204 getExisting: Effect.succeed(Option.none()),1204 getExisting: Effect.succeedNone,
1205 hasCredential: Effect.succeed(false),1205 hasCredential: Effect.succeed(false),
1206 clear: Effect.void,1206 clear: Effect.void,
1207 ...options?.layers?.cloudCliTokenManager,1207 ...options?.layers?.cloudCliTokenManager,
...@@ -8972,8 +8972,7 @@ it.layer(NodeServices.layer)("server router seam", (it) => {...@@ -8972,8 +8972,7 @@ it.layer(NodeServices.layer)("server router seam", (it) => {
8972 yield* buildAppUnderTest({8972 yield* buildAppUnderTest({
8973 layers: {8973 layers: {
8974 projectionSnapshotQuery: {8974 projectionSnapshotQuery: {
8975 getThreadDetailSnapshot: () =>8975 getThreadDetailSnapshot: () => Effect.succeedSome({ snapshotSequence: 1, thread }),
8976 Effect.succeed(Option.some({ snapshotSequence: 1, thread })),
8977 },8976 },
8978 },8977 },
8979 });8978 });
...@@ -9146,16 +9145,13 @@ it.layer(NodeServices.layer)("server router seam", (it) => {...@@ -9146,16 +9145,13 @@ it.layer(NodeServices.layer)("server router seam", (it) => {
9146 streamDomainEvents: Stream.concat(Stream.make(event), Stream.never),9145 streamDomainEvents: Stream.concat(Stream.make(event), Stream.never),
9147 },9146 },
9148 projectionSnapshotQuery: {9147 projectionSnapshotQuery: {
9149 getThreadDetailSnapshot: () =>9148 getThreadDetailSnapshot: () => Effect.succeedSome({ snapshotSequence: 1, thread }),
9150 Effect.succeed(Option.some({ snapshotSequence: 1, thread })),
9151 getThreadShellById: (threadId) =>9149 getThreadShellById: (threadId) =>
9152 Effect.succeed(9150 Effect.succeedSome({
9153 Option.some({9151 ...makeDefaultOrchestrationThreadShell(),
9154 ...makeDefaultOrchestrationThreadShell(),9152 id: threadId,
9155 id: threadId,9153 title: "Build complete",
9156 title: "Build complete",9154 }),
9157 }),
9158 ),
9159 },9155 },
9160 },9156 },
9161 });9157 });
...@@ -9433,7 +9429,7 @@ it.layer(NodeServices.layer)("server router seam", (it) => {...@@ -9433,7 +9429,7 @@ it.layer(NodeServices.layer)("server router seam", (it) => {
9433 },9429 },
9434 projectionSnapshotQuery: {9430 projectionSnapshotQuery: {
9435 getThreadDetailSnapshot: () =>9431 getThreadDetailSnapshot: () =>
9436 Effect.succeed(Option.some({ snapshotSequence: 100_000, thread })),9432 Effect.succeedSome({ snapshotSequence: 100_000, thread }),
9437 },9433 },
9438 },9434 },
9439 });9435 });
...@@ -9751,8 +9747,7 @@ it.layer(NodeServices.layer)("server router seam", (it) => {...@@ -9751,8 +9747,7 @@ it.layer(NodeServices.layer)("server router seam", (it) => {
9751 }),9747 }),
9752 },9748 },
9753 projectionSnapshotQuery: {9749 projectionSnapshotQuery: {
9754 getThreadDetailSnapshot: () =>9750 getThreadDetailSnapshot: () => Effect.succeedSome({ snapshotSequence: 5, thread }),
9755 Effect.succeed(Option.some({ snapshotSequence: 5, thread })),
9756 },9751 },
9757 },9752 },
9758 });9753 });
...@@ -9840,7 +9835,7 @@ it.layer(NodeServices.layer)("server router seam", (it) => {...@@ -9840,7 +9835,7 @@ it.layer(NodeServices.layer)("server router seam", (it) => {
9840 projectionSnapshotQuery: {9835 projectionSnapshotQuery: {
9841 getThreadDetailSnapshot: (_threadId, options) => {9836 getThreadDetailSnapshot: (_threadId, options) => {
9842 requestedTurnLimit = options?.turnLimit;9837 requestedTurnLimit = options?.turnLimit;
9843 return Effect.succeed(Option.some({ snapshotSequence: 5, thread }));9838 return Effect.succeedSome({ snapshotSequence: 5, thread });
9844 },9839 },
9845 },9840 },
9846 },9841 },
...@@ -9933,7 +9928,7 @@ it.layer(NodeServices.layer)("server router seam", (it) => {...@@ -9933,7 +9928,7 @@ it.layer(NodeServices.layer)("server router seam", (it) => {
9933 readEvents: store.readFromSequence,9928 readEvents: store.readFromSequence,
9934 },9929 },
9935 projectionSnapshotQuery: {9930 projectionSnapshotQuery: {
9936 getThreadDetailSnapshot: () => Effect.succeed(Option.none()),9931 getThreadDetailSnapshot: () => Effect.succeedNone,
9937 },9932 },
9938 },9933 },
9939 });9934 });
...@@ -10163,8 +10158,7 @@ it.layer(NodeServices.layer)("server router seam", (it) => {...@@ -10163,8 +10158,7 @@ it.layer(NodeServices.layer)("server router seam", (it) => {
10163 replayStatsCalls += 1;10158 replayStatsCalls += 1;
10164 return { eventCount: 5, payloadBytes: 8 * 1024 * 1024 + 1 };10159 return { eventCount: 5, payloadBytes: 8 * 1024 * 1024 + 1 };
10165 }),10160 }),
10166 getThreadDetailSnapshot: () =>10161 getThreadDetailSnapshot: () => Effect.succeedSome({ snapshotSequence: 5, thread }),
10167 Effect.succeed(Option.some({ snapshotSequence: 5, thread })),
10168 getShellSnapshot: () =>10162 getShellSnapshot: () =>
10169 Effect.succeed({10163 Effect.succeed({
10170 snapshotSequence: 5,10164 snapshotSequence: 5,
...@@ -10468,7 +10462,7 @@ it.layer(NodeServices.layer)("server router seam", (it) => {...@@ -10468,7 +10462,7 @@ it.layer(NodeServices.layer)("server router seam", (it) => {
10468 ]),10462 ]),
10469 },10463 },
10470 projectionSnapshotQuery: {10464 projectionSnapshotQuery: {
10471 getThreadShellById: () => Effect.succeed(Option.none()),10465 getThreadShellById: () => Effect.succeedNone,
10472 },10466 },
10473 },10467 },
10474 });10468 });
...@@ -10526,9 +10520,7 @@ it.layer(NodeServices.layer)("server router seam", (it) => {...@@ -10526,9 +10520,7 @@ it.layer(NodeServices.layer)("server router seam", (it) => {
10526 detail: "transient failure",10520 detail: "transient failure",
10527 }),10521 }),
10528 )10522 )
10529 : Effect.succeed(10523 : Effect.succeedSome(makeDefaultOrchestrationThreadShell({ id: threadId }));
10530 Option.some(makeDefaultOrchestrationThreadShell({ id: threadId })),
10531 );
10532 }),10524 }),
10533 },10525 },
10534 },10526 },
...@@ -10588,7 +10580,7 @@ it.layer(NodeServices.layer)("server router seam", (it) => {...@@ -10588,7 +10580,7 @@ it.layer(NodeServices.layer)("server router seam", (it) => {
10588 ]),10580 ]),
10589 },10581 },
10590 projectionSnapshotQuery: {10582 projectionSnapshotQuery: {
10591 getProjectShellById: () => Effect.succeed(Option.none()),10583 getProjectShellById: () => Effect.succeedNone,
10592 },10584 },
10593 },10585 },
10594 });10586 });
...@@ -10634,22 +10626,20 @@ it.layer(NodeServices.layer)("server router seam", (it) => {...@@ -10634,22 +10626,20 @@ it.layer(NodeServices.layer)("server router seam", (it) => {
10634 },10626 },
10635 projectionSnapshotQuery: {10627 projectionSnapshotQuery: {
10636 getThreadShellById: () =>10628 getThreadShellById: () =>
10637 Effect.succeed(10629 Effect.succeedSome(
10638 Option.some(10630 makeDefaultOrchestrationThreadShell({
10639 makeDefaultOrchestrationThreadShell({10631 id: threadId,
10640 id: threadId,10632 updatedAt: now,
10633 session: {
10634 threadId,
10635 status: "ready",
10636 providerName: "claudeAgent",
10637 runtimeMode: "full-access",
10638 activeTurnId: null,
10639 lastError: null,
10641 updatedAt: now,10640 updatedAt: now,
10642 session: {10641 },
10643 threadId,10642 }),
10644 status: "ready",
10645 providerName: "claudeAgent",
10646 runtimeMode: "full-access",
10647 activeTurnId: null,
10648 lastError: null,
10649 updatedAt: now,
10650 },
10651 }),
10652 ),
10653 ),10643 ),
10654 },10644 },
10655 },10645 },
...@@ -10782,8 +10772,8 @@ it.layer(NodeServices.layer)("server router seam", (it) => {...@@ -10782,8 +10772,8 @@ it.layer(NodeServices.layer)("server router seam", (it) => {
10782 },10772 },
10783 projectionSnapshotQuery: {10773 projectionSnapshotQuery: {
10784 getThreadShellById: () =>10774 getThreadShellById: () =>
10785 Effect.succeed(10775 Effect.succeedSome(
10786 Option.some(makeDefaultOrchestrationThreadShell({ id: threadId, session: null })),10776 makeDefaultOrchestrationThreadShell({ id: threadId, session: null }),
10787 ),10777 ),
10788 },10778 },
10789 },10779 },
...@@ -10836,22 +10826,20 @@ it.layer(NodeServices.layer)("server router seam", (it) => {...@@ -10836,22 +10826,20 @@ it.layer(NodeServices.layer)("server router seam", (it) => {
10836 },10826 },
10837 projectionSnapshotQuery: {10827 projectionSnapshotQuery: {
10838 getThreadShellById: () =>10828 getThreadShellById: () =>
10839 Effect.succeed(10829 Effect.succeedSome(
10840 Option.some(10830 makeDefaultOrchestrationThreadShell({
10841 makeDefaultOrchestrationThreadShell({10831 id: threadId,
10842 id: threadId,10832 updatedAt: now,
10833 session: {
10834 threadId,
10835 status: "stopped",
10836 providerName: "claudeAgent",
10837 runtimeMode: "full-access",
10838 activeTurnId: null,
10839 lastError: null,
10843 updatedAt: now,10840 updatedAt: now,
10844 session: {10841 },
10845 threadId,10842 }),
10846 status: "stopped",
10847 providerName: "claudeAgent",
10848 runtimeMode: "full-access",
10849 activeTurnId: null,
10850 lastError: null,
10851 updatedAt: now,
10852 },
10853 }),
10854 ),
10855 ),10843 ),
10856 },10844 },
10857 },10845 },
...@@ -10902,22 +10890,20 @@ it.layer(NodeServices.layer)("server router seam", (it) => {...@@ -10902,22 +10890,20 @@ it.layer(NodeServices.layer)("server router seam", (it) => {
10902 },10890 },
10903 projectionSnapshotQuery: {10891 projectionSnapshotQuery: {
10904 getThreadShellById: () =>10892 getThreadShellById: () =>
10905 Effect.succeed(10893 Effect.succeedSome(
10906 Option.some(10894 makeDefaultOrchestrationThreadShell({
10907 makeDefaultOrchestrationThreadShell({10895 id: threadId,
10908 id: threadId,10896 updatedAt: now,
10897 session: {
10898 threadId,
10899 status: "ready",
10900 providerName: "claudeAgent",
10901 runtimeMode: "full-access",
10902 activeTurnId: null,
10903 lastError: null,
10909 updatedAt: now,10904 updatedAt: now,
10910 session: {10905 },
10911 threadId,10906 }),
10912 status: "ready",
10913 providerName: "claudeAgent",
10914 runtimeMode: "full-access",
10915 activeTurnId: null,
10916 lastError: null,
10917 updatedAt: now,
10918 },
10919 }),
10920 ),
10921 ),10907 ),
10922 },10908 },
10923 },10909 },
...@@ -11005,22 +10991,20 @@ it.layer(NodeServices.layer)("server router seam", (it) => {...@@ -11005,22 +10991,20 @@ it.layer(NodeServices.layer)("server router seam", (it) => {
11005 },10991 },
11006 projectionSnapshotQuery: {10992 projectionSnapshotQuery: {
11007 getThreadShellById: () =>10993 getThreadShellById: () =>
11008 Effect.succeed(10994 Effect.succeedSome(
11009 Option.some(10995 makeDefaultOrchestrationThreadShell({
11010 makeDefaultOrchestrationThreadShell({10996 id: threadId,
11011 id: threadId,10997 updatedAt: now,
10998 session: {
10999 threadId,
11000 status: "ready",
11001 providerName: "claudeAgent",
11002 runtimeMode: "full-access",
11003 activeTurnId: null,
11004 lastError: null,
11012 updatedAt: now,11005 updatedAt: now,
11013 session: {11006 },
11014 threadId,11007 }),
11015 status: "ready",
11016 providerName: "claudeAgent",
11017 runtimeMode: "full-access",
11018 activeTurnId: null,
11019 lastError: null,
11020 updatedAt: now,
11021 },
11022 }),
11023 ),
11024 ),11008 ),
11025 },11009 },
11026 },11010 },
...@@ -11077,22 +11061,20 @@ it.layer(NodeServices.layer)("server router seam", (it) => {...@@ -11077,22 +11061,20 @@ it.layer(NodeServices.layer)("server router seam", (it) => {
11077 },11061 },
11078 projectionSnapshotQuery: {11062 projectionSnapshotQuery: {
11079 getThreadShellById: () =>11063 getThreadShellById: () =>
11080 Effect.succeed(11064 Effect.succeedSome(
11081 Option.some(11065 makeDefaultOrchestrationThreadShell({
11082 makeDefaultOrchestrationThreadShell({11066 id: threadId,
11083 id: threadId,11067 updatedAt: now,
11068 session: {
11069 threadId,
11070 status: "ready",
11071 providerName: "claudeAgent",
11072 runtimeMode: "full-access",
11073 activeTurnId: null,
11074 lastError: null,
11084 updatedAt: now,11075 updatedAt: now,
11085 session: {11076 },
11086 threadId,11077 }),
11087 status: "ready",
11088 providerName: "claudeAgent",
11089 runtimeMode: "full-access",
11090 activeTurnId: null,
11091 lastError: null,
11092 updatedAt: now,
11093 },
11094 }),
11095 ),
11096 ),11078 ),
11097 },11079 },
11098 },11080 },
apps/server/src/serverRuntimeStartup.reconcile.test.ts+39-49
...@@ -399,18 +399,16 @@ it.effect("does not continue archived or deleted marked sessions", () => {...@@ -399,18 +399,16 @@ it.effect("does not continue archived or deleted marked sessions", () => {
399 directory: {399 directory: {
400 getBinding: (threadId) => {400 getBinding: (threadId) => {
401 const thread = threadId === archived.id ? archived : deleted;401 const thread = threadId === archived.id ? archived : deleted;
402 return Effect.succeed(402 return Effect.succeedSome({
403 Option.some({403 threadId,
404 threadId,404 provider: ProviderDriverKind.make("codex"),
405 provider: ProviderDriverKind.make("codex"),405 providerInstanceId,
406 providerInstanceId,406 status: "running" as const,
407 status: "running" as const,407 resumeCursor: { cursor: threadId },
408 resumeCursor: { cursor: threadId },408 runtimePayload: {
409 runtimePayload: {409 continueAfterServerUpdate: thread.session.activeTurnId,
410 continueAfterServerUpdate: thread.session.activeTurnId,410 },
411 },411 });
412 }),
413 );
414 },412 },
415 upsert: () => Effect.void,413 upsert: () => Effect.void,
416 recordImportedTranscript: () => Effect.die("unused"),414 recordImportedTranscript: () => Effect.die("unused"),
...@@ -456,18 +454,16 @@ it.effect("retries continuation preparation before settling a persistent failure...@@ -456,18 +454,16 @@ it.effect("retries continuation preparation before settling a persistent failure
456 threads: [thread],454 threads: [thread],
457 directory: {455 directory: {
458 getBinding: () =>456 getBinding: () =>
459 Effect.succeed(457 Effect.succeedSome({
460 Option.some({458 threadId: thread.id,
461 threadId: thread.id,459 provider: ProviderDriverKind.make("codex"),
462 provider: ProviderDriverKind.make("codex"),460 providerInstanceId,
463 providerInstanceId,461 status: "running" as const,
464 status: "running" as const,462 resumeCursor: { cursor: thread.id },
465 resumeCursor: { cursor: thread.id },463 runtimePayload: {
466 runtimePayload: {464 continueAfterServerUpdate: thread.session.activeTurnId,
467 continueAfterServerUpdate: thread.session.activeTurnId,465 },
468 },466 }),
469 }),
470 ),
471 upsert: () => Effect.void,467 upsert: () => Effect.void,
472 recordImportedTranscript: () => Effect.die("unused"),468 recordImportedTranscript: () => Effect.die("unused"),
473 getProvider: () => Effect.die("unused"),469 getProvider: () => Effect.die("unused"),
...@@ -610,16 +606,14 @@ it.effect(...@@ -610,16 +606,14 @@ it.effect(
610 directory: {606 directory: {
611 getBinding: (candidate) =>607 getBinding: (candidate) =>
612 candidate === absent.id608 candidate === absent.id
613 ? Effect.succeed(Option.none())609 ? Effect.succeedNone
614 : candidate === corrupt.id610 : candidate === corrupt.id
615 ? Effect.fail(corruptFailure)611 ? Effect.fail(corruptFailure)
616 : Effect.succeed(612 : Effect.succeedSome({
617 Option.some({613 threadId: candidate,
618 threadId: candidate,614 provider: ProviderDriverKind.make("codex"),
619 provider: ProviderDriverKind.make("codex"),615 providerInstanceId,
620 providerInstanceId,616 }),
621 }),
622 ),
623 upsert: () => Effect.fail(writeFailure),617 upsert: () => Effect.fail(writeFailure),
624 recordImportedTranscript: () => Effect.die("unused"),618 recordImportedTranscript: () => Effect.die("unused"),
625 getProvider: () => Effect.die("unused"),619 getProvider: () => Effect.die("unused"),
...@@ -657,7 +651,7 @@ it.effect("retries failed projections and continues after a persistent failure",...@@ -657,7 +651,7 @@ it.effect("retries failed projections and continues after a persistent failure",
657 return runReconciliation({651 return runReconciliation({
658 threads: [transient, persistent, later],652 threads: [transient, persistent, later],
659 directory: {653 directory: {
660 getBinding: () => Effect.succeed(Option.none()),654 getBinding: () => Effect.succeedNone,
661 upsert: () => Effect.void,655 upsert: () => Effect.void,
662 recordImportedTranscript: () => Effect.die("unused"),656 recordImportedTranscript: () => Effect.die("unused"),
663 getProvider: () => Effect.die("unused"),657 getProvider: () => Effect.die("unused"),
...@@ -758,19 +752,17 @@ for (const scenario of [...@@ -758,19 +752,17 @@ for (const scenario of [
758 continueAfterRestart: scenario !== "disabled",752 continueAfterRestart: scenario !== "disabled",
759 directory: {753 directory: {
760 getBinding: () =>754 getBinding: () =>
761 Effect.succeed(755 Effect.succeedSome({
762 Option.some({756 threadId: thread.id,
763 threadId: thread.id,757 provider: ProviderDriverKind.make("codex"),
764 provider: ProviderDriverKind.make("codex"),758 providerInstanceId,
765 providerInstanceId,759 status: scenario === "stopped binding" ? "stopped" : "running",
766 status: scenario === "stopped binding" ? "stopped" : "running",760 ...(scenario.includes("cursor") ? {} : { resumeCursor: { threadId: thread.id } }),
767 ...(scenario.includes("cursor") ? {} : { resumeCursor: { threadId: thread.id } }),761 runtimePayload: {
768 runtimePayload: {762 activeTurnId: scenario === "marked superseded turn" ? "another-turn" : turnId,
769 activeTurnId: scenario === "marked superseded turn" ? "another-turn" : turnId,763 ...(scenario.startsWith("marked") ? { continueAfterServerUpdate: turnId } : {}),
770 ...(scenario.startsWith("marked") ? { continueAfterServerUpdate: turnId } : {}),764 },
771 },765 }),
772 }),
773 ),
774 upsert: (binding) =>766 upsert: (binding) =>
775 Effect.sync(() => {767 Effect.sync(() => {
776 upserts.push(binding);768 upserts.push(binding);
...@@ -944,9 +936,7 @@ it.effect("settles failed opt-in recovery without retrying the provider turn", (...@@ -944,9 +936,7 @@ it.effect("settles failed opt-in recovery without retrying the provider turn", (
944 Effect.gen(function* () {936 Effect.gen(function* () {
945 sends.push(input);937 sends.push(input);
946 preparedPayloads.push(binding.runtimePayload);938 preparedPayloads.push(binding.runtimePayload);
947 return yield* Effect.fail(939 return yield* new ProviderSessionNotFoundError({ threadId: input.threadId });
948 new ProviderSessionNotFoundError({ threadId: input.threadId }),
949 );
950 }),940 }),
951 },941 },
952 directory: {942 directory: {
apps/server/src/serverRuntimeStartup.test.ts+26-28
...@@ -174,27 +174,25 @@ it.effect("resolveAutoBootstrapWelcomeTargets returns existing project and threa...@@ -174,27 +174,25 @@ it.effect("resolveAutoBootstrapWelcomeTargets returns existing project and threa
174 getCounts: () => Effect.die("unused"),174 getCounts: () => Effect.die("unused"),
175 getEventReplayStats: () => Effect.die("unused"),175 getEventReplayStats: () => Effect.die("unused"),
176 getActiveProjectByWorkspaceRoot: () =>176 getActiveProjectByWorkspaceRoot: () =>
177 Effect.succeed(177 Effect.succeedSome({
178 Option.some({178 id: bootstrapProjectId,
179 id: bootstrapProjectId,179 title: "Startup Project",
180 title: "Startup Project",180 workspaceRoot: "/tmp/startup-project",
181 workspaceRoot: "/tmp/startup-project",181 defaultModelSelection: {
182 defaultModelSelection: {182 instanceId: ProviderInstanceId.make("codex"),
183 instanceId: ProviderInstanceId.make("codex"),183 model: DEFAULT_MODEL,
184 model: DEFAULT_MODEL,184 },
185 },185 scripts: [],
186 scripts: [],186 createdAt: "2026-01-01T00:00:00.000Z",
187 createdAt: "2026-01-01T00:00:00.000Z",187 updatedAt: "2026-01-01T00:00:00.000Z",
188 updatedAt: "2026-01-01T00:00:00.000Z",188 deletedAt: null,
189 deletedAt: null,189 }),
190 }),
191 ),
192 getProjectShells: () => Effect.die("unused"),190 getProjectShells: () => Effect.die("unused"),
193 getProjectShellById: () => Effect.die("unused"),191 getProjectShellById: () => Effect.die("unused"),
194 getFirstActiveThreadIdByProjectId: () => Effect.succeed(Option.some(bootstrapThreadId)),192 getFirstActiveThreadIdByProjectId: () => Effect.succeedSome(bootstrapThreadId),
195 getImportedAgentSessionSources: () => Effect.die("unused"),193 getImportedAgentSessionSources: () => Effect.die("unused"),
196 getThreadCheckpointContext: () => Effect.succeed(Option.none()),194 getThreadCheckpointContext: () => Effect.succeedNone,
197 getFullThreadDiffContext: () => Effect.succeed(Option.none()),195 getFullThreadDiffContext: () => Effect.succeedNone,
198 getThreadRuntimeContext: () => Effect.die("unused"),196 getThreadRuntimeContext: () => Effect.die("unused"),
199 getTurnStartMessage: () => Effect.die("unused"),197 getTurnStartMessage: () => Effect.die("unused"),
200 getThreadShellById: () => Effect.die("unused"),198 getThreadShellById: () => Effect.die("unused"),
...@@ -321,10 +319,10 @@ it.effect.each([...@@ -321,10 +319,10 @@ it.effect.each([
321 ),319 ),
322 getProjectShells: () => Effect.die("unused"),320 getProjectShells: () => Effect.die("unused"),
323 getProjectShellById: () => Effect.die("unused"),321 getProjectShellById: () => Effect.die("unused"),
324 getFirstActiveThreadIdByProjectId: () => Effect.succeed(Option.none()),322 getFirstActiveThreadIdByProjectId: () => Effect.succeedNone,
325 getImportedAgentSessionSources: () => Effect.die("unused"),323 getImportedAgentSessionSources: () => Effect.die("unused"),
326 getThreadCheckpointContext: () => Effect.succeed(Option.none()),324 getThreadCheckpointContext: () => Effect.succeedNone,
327 getFullThreadDiffContext: () => Effect.succeed(Option.none()),325 getFullThreadDiffContext: () => Effect.succeedNone,
328 getThreadRuntimeContext: () => Effect.die("unused"),326 getThreadRuntimeContext: () => Effect.die("unused"),
329 getTurnStartMessage: () => Effect.die("unused"),327 getTurnStartMessage: () => Effect.die("unused"),
330 getThreadShellById: () => Effect.die("unused"),328 getThreadShellById: () => Effect.die("unused"),
...@@ -391,13 +389,13 @@ it.effect(...@@ -391,13 +389,13 @@ it.effect(
391 getSnapshotSequence: () => Effect.die("unused"),389 getSnapshotSequence: () => Effect.die("unused"),
392 getCounts: () => Effect.die("unused"),390 getCounts: () => Effect.die("unused"),
393 getEventReplayStats: () => Effect.die("unused"),391 getEventReplayStats: () => Effect.die("unused"),
394 getActiveProjectByWorkspaceRoot: () => Effect.succeed(Option.none()),392 getActiveProjectByWorkspaceRoot: () => Effect.succeedNone,
395 getProjectShells: () => Effect.die("unused"),393 getProjectShells: () => Effect.die("unused"),
396 getProjectShellById: () => Effect.die("unused"),394 getProjectShellById: () => Effect.die("unused"),
397 getFirstActiveThreadIdByProjectId: () => Effect.die("thread lookup failed"),395 getFirstActiveThreadIdByProjectId: () => Effect.die("thread lookup failed"),
398 getImportedAgentSessionSources: () => Effect.die("unused"),396 getImportedAgentSessionSources: () => Effect.die("unused"),
399 getThreadCheckpointContext: () => Effect.succeed(Option.none()),397 getThreadCheckpointContext: () => Effect.succeedNone,
400 getFullThreadDiffContext: () => Effect.succeed(Option.none()),398 getFullThreadDiffContext: () => Effect.succeedNone,
401 getThreadRuntimeContext: () => Effect.die("unused"),399 getThreadRuntimeContext: () => Effect.die("unused"),
402 getTurnStartMessage: () => Effect.die("unused"),400 getTurnStartMessage: () => Effect.die("unused"),
403 getThreadShellById: () => Effect.die("unused"),401 getThreadShellById: () => Effect.die("unused"),
...@@ -456,13 +454,13 @@ it.effect("resolveAutoBootstrapWelcomeTargets preserves typed UUID generation fa...@@ -456,13 +454,13 @@ it.effect("resolveAutoBootstrapWelcomeTargets preserves typed UUID generation fa
456 getSnapshotSequence: () => Effect.die("unused"),454 getSnapshotSequence: () => Effect.die("unused"),
457 getCounts: () => Effect.die("unused"),455 getCounts: () => Effect.die("unused"),
458 getEventReplayStats: () => Effect.die("unused"),456 getEventReplayStats: () => Effect.die("unused"),
459 getActiveProjectByWorkspaceRoot: () => Effect.succeed(Option.none()),457 getActiveProjectByWorkspaceRoot: () => Effect.succeedNone,
460 getProjectShells: () => Effect.die("unused"),458 getProjectShells: () => Effect.die("unused"),
461 getProjectShellById: () => Effect.die("unused"),459 getProjectShellById: () => Effect.die("unused"),
462 getFirstActiveThreadIdByProjectId: () => Effect.succeed(Option.none()),460 getFirstActiveThreadIdByProjectId: () => Effect.succeedNone,
463 getImportedAgentSessionSources: () => Effect.die("unused"),461 getImportedAgentSessionSources: () => Effect.die("unused"),
464 getThreadCheckpointContext: () => Effect.succeed(Option.none()),462 getThreadCheckpointContext: () => Effect.succeedNone,
465 getFullThreadDiffContext: () => Effect.succeed(Option.none()),463 getFullThreadDiffContext: () => Effect.succeedNone,
466 getThreadRuntimeContext: () => Effect.die("unused"),464 getThreadRuntimeContext: () => Effect.die("unused"),
467 getTurnStartMessage: () => Effect.die("unused"),465 getTurnStartMessage: () => Effect.die("unused"),
468 getThreadShellById: () => Effect.die("unused"),466 getThreadShellById: () => Effect.die("unused"),
apps/server/src/serverRuntimeStartup.ts+45-49
...@@ -264,13 +264,13 @@ export const resolveAutoBootstrapWelcomeTargets = Effect.gen(function* () {...@@ -264,13 +264,13 @@ export const resolveAutoBootstrapWelcomeTargets = Effect.gen(function* () {
264 bootstrapThreadId = existingThreadId.value;264 bootstrapThreadId = existingThreadId.value;
265 }265 }
266 }).pipe(266 }).pipe(
267 Effect.catchCause((cause) =>267 Effect.catchCauseIf(
268 Cause.hasInterrupts(cause)268 (cause) => !Cause.hasInterrupts(cause),
269 ? Effect.failCause(cause)269 (cause) =>
270 : Effect.logWarning("startup thread auto-bootstrap failed", {270 Effect.logWarning("startup thread auto-bootstrap failed", {
271 bootstrapProjectId: nextProjectId,271 bootstrapProjectId: nextProjectId,
272 cause,272 cause,
273 }),273 }),
274 ),274 ),
275 );275 );
276 });276 });
...@@ -312,11 +312,9 @@ const resolveStartupBrowserTarget = Effect.gen(function* () {...@@ -312,11 +312,9 @@ const resolveStartupBrowserTarget = Effect.gen(function* () {
312 ? `http://${formatHostForUrl(serverConfig.host)}:${serverConfig.port}`312 ? `http://${formatHostForUrl(serverConfig.host)}:${serverConfig.port}`
313 : localUrl;313 : localUrl;
314 const baseTarget = serverConfig.devUrl?.toString() ?? bindUrl;314 const baseTarget = serverConfig.devUrl?.toString() ?? bindUrl;
315 return yield* Effect.succeed(serverConfig.mode === "desktop" ? baseTarget : undefined).pipe(315 return serverConfig.mode === "desktop"
316 Effect.flatMap((target) =>316 ? baseTarget
317 target ? Effect.succeed(target) : serverAuth.issueStartupPairingUrl(baseTarget),317 : yield* serverAuth.issueStartupPairingUrl(baseTarget);
318 ),
319 );
320});318});
321319
322const maybeOpenBrowser = (target: string) =>320const maybeOpenBrowser = (target: string) =>
...@@ -487,7 +485,7 @@ export const reconcileProviderSessions = Effect.gen(function* () {...@@ -487,7 +485,7 @@ export const reconcileProviderSessions = Effect.gen(function* () {
487 const query = yield* ProjectionSnapshotQuery.ProjectionSnapshotQuery;485 const query = yield* ProjectionSnapshotQuery.ProjectionSnapshotQuery;
488 const settings = yield* ServerSettings.ServerSettingsService;486 const settings = yield* ServerSettings.ServerSettingsService;
489 const restartSettings = yield* settings.getSettings.pipe(487 const restartSettings = yield* settings.getSettings.pipe(
490 Effect.map(Option.some),488 Effect.asSome,
491 Effect.catch((cause) =>489 Effect.catch((cause) =>
492 Effect.logWarning("could not read restart continuation preference", { cause }).pipe(490 Effect.logWarning("could not read restart continuation preference", { cause }).pipe(
493 Effect.as(Option.none()),491 Effect.as(Option.none()),
...@@ -549,13 +547,13 @@ export const reconcileProviderSessions = Effect.gen(function* () {...@@ -549,13 +547,13 @@ export const reconcileProviderSessions = Effect.gen(function* () {
549 continue;547 continue;
550 }548 }
551 const binding = yield* directory.getBinding(thread.id).pipe(549 const binding = yield* directory.getBinding(thread.id).pipe(
552 Effect.catchCause((cause) =>550 Effect.catchCauseIf(
553 Cause.hasInterrupts(cause)551 (cause) => !Cause.hasInterrupts(cause),
554 ? Effect.failCause(cause)552 (cause) =>
555 : Effect.logWarning("failed to read orphaned provider session directory binding", {553 Effect.logWarning("failed to read orphaned provider session directory binding", {
556 threadId: thread.id,554 threadId: thread.id,
557 cause,555 cause,
558 }).pipe(Effect.as(Option.none())),556 }).pipe(Effect.as(Option.none())),
559 ),557 ),
560 );558 );
561 const continuationMarkerPresent =559 const continuationMarkerPresent =
...@@ -606,13 +604,13 @@ export const reconcileProviderSessions = Effect.gen(function* () {...@@ -606,13 +604,13 @@ export const reconcileProviderSessions = Effect.gen(function* () {
606 });604 });
607 }605 }
608 }).pipe(606 }).pipe(
609 Effect.catchCause((cause) =>607 Effect.catchCauseIf(
610 Cause.hasInterrupts(cause)608 (cause) => !Cause.hasInterrupts(cause),
611 ? Effect.failCause(cause)609 (cause) =>
612 : Effect.logWarning(610 Effect.logWarning("failed to reconcile orphaned provider session directory binding", {
613 "failed to reconcile orphaned provider session directory binding",611 threadId: thread.id,
614 { threadId: thread.id, cause },612 cause,
615 ),613 }),
616 ),614 ),
617 );615 );
618616
...@@ -633,13 +631,13 @@ export const reconcileProviderSessions = Effect.gen(function* () {...@@ -633,13 +631,13 @@ export const reconcileProviderSessions = Effect.gen(function* () {
633 });631 });
634 }).pipe(632 }).pipe(
635 Effect.retry({ times: 1 }),633 Effect.retry({ times: 1 }),
636 Effect.catchCause((cause) =>634 Effect.catchCauseIf(
637 Cause.hasInterrupts(cause)635 (cause) => !Cause.hasInterrupts(cause),
638 ? Effect.failCause(cause)636 (cause) =>
639 : Effect.logWarning("failed to settle orphaned provider session projection", {637 Effect.logWarning("failed to settle orphaned provider session projection", {
640 threadId: thread.id,638 threadId: thread.id,
641 cause,639 cause,
642 }),640 }),
643 ),641 ),
644 );642 );
645 });643 });
...@@ -739,10 +737,9 @@ export const reconcileProviderSessions = Effect.gen(function* () {...@@ -739,10 +737,9 @@ export const reconcileProviderSessions = Effect.gen(function* () {
739 yield* settleAsError(ORPHANED_PROVIDER_SESSION_ERROR);737 yield* settleAsError(ORPHANED_PROVIDER_SESSION_ERROR);
740 }738 }
741}).pipe(739}).pipe(
742 Effect.catchCause((cause) =>740 Effect.catchCauseIf(
743 Cause.hasInterrupts(cause)741 (cause) => !Cause.hasInterrupts(cause),
744 ? Effect.failCause(cause)742 (cause) => Effect.logWarning("provider session startup reconciliation failed", { cause }),
745 : Effect.logWarning("provider session startup reconciliation failed", { cause }),
746 ),743 ),
747);744);
748745
...@@ -813,21 +810,20 @@ export const reconcileWorktreeSetups = Effect.gen(function* () {...@@ -813,21 +810,20 @@ export const reconcileWorktreeSetups = Effect.gen(function* () {
813 createdAt: interruptedAt,810 createdAt: interruptedAt,
814 })811 })
815 .pipe(812 .pipe(
816 Effect.catchCause((cause) =>813 Effect.catchCauseIf(
817 Cause.hasInterrupts(cause)814 (cause) => !Cause.hasInterrupts(cause),
818 ? Effect.failCause(cause)815 (cause) =>
819 : Effect.logWarning("failed to settle interrupted worktree setup", {816 Effect.logWarning("failed to settle interrupted worktree setup", {
820 threadId,817 threadId,
821 cause,818 cause,
822 }),819 }),
823 ),820 ),
824 );821 );
825 }822 }
826}).pipe(823}).pipe(
827 Effect.catchCause((cause) =>824 Effect.catchCauseIf(
828 Cause.hasInterrupts(cause)825 (cause) => !Cause.hasInterrupts(cause),
829 ? Effect.failCause(cause)826 (cause) => Effect.logWarning("worktree setup startup reconciliation failed", { cause }),
830 : Effect.logWarning("worktree setup startup reconciliation failed", { cause }),
831 ),827 ),
832);828);
833829
apps/server/src/serverRuntimeState.ts+2-2
...@@ -142,7 +142,7 @@ export const readPersistedServerRuntimeState = (path: string) =>...@@ -142,7 +142,7 @@ export const readPersistedServerRuntimeState = (path: string) =>
142 cause,142 cause,
143 }),143 }),
144 ),144 ),
145 onSuccess: (contents) => Effect.succeed(Option.some(contents)),145 onSuccess: (contents) => Effect.succeedSome(contents),
146 }),146 }),
147 );147 );
148 if (Option.isNone(raw)) {148 if (Option.isNone(raw)) {
...@@ -155,7 +155,7 @@ export const readPersistedServerRuntimeState = (path: string) =>...@@ -155,7 +155,7 @@ export const readPersistedServerRuntimeState = (path: string) =>
155 }155 }
156156
157 return yield* decodePersistedServerRuntimeState(trimmed).pipe(157 return yield* decodePersistedServerRuntimeState(trimmed).pipe(
158 Effect.map(Option.some),158 Effect.asSome,
159 Effect.mapError(159 Effect.mapError(
160 (cause) =>160 (cause) =>
161 new ServerRuntimeStateError({161 new ServerRuntimeStateError({
apps/server/src/sourceControl/GitHubCli.test.ts+3-3
...@@ -155,8 +155,9 @@ describe("GitHubCli.layer", () => {...@@ -155,8 +155,9 @@ describe("GitHubCli.layer", () => {
155 capacity: 2,155 capacity: 2,
156 timeToLive: "1 minute",156 timeToLive: "1 minute",
157 });157 });
158 const results = yield* Effect.all(158 const results = yield* Effect.forEach(
159 ["github.com", "github.example.test"].map((host, index) =>159 ["github.com", "github.example.test"],
160 (host, index) =>
160 Cache.get(cache, host).pipe(161 Cache.get(cache, host).pipe(
161 Effect.provideService(GitHubCli.PinnedGitHubCredential, {162 Effect.provideService(GitHubCli.PinnedGitHubCredential, {
162 host,163 host,
...@@ -164,7 +165,6 @@ describe("GitHubCli.layer", () => {...@@ -164,7 +165,6 @@ describe("GitHubCli.layer", () => {
164 credentialFingerprint: `fingerprint-${index}`,165 credentialFingerprint: `fingerprint-${index}`,
165 }),166 }),
166 ),167 ),
167 ),
168 { concurrency: 2 },168 { concurrency: 2 },
169 );169 );
170 expect(results.map((result) => result.stdout)).toEqual(["credential-0", "credential-1"]);170 expect(results.map((result) => result.stdout)).toEqual(["credential-0", "credential-1"]);
apps/server/src/sourceControl/SourceControlProviderRegistry.ts+3-3
...@@ -289,14 +289,14 @@ export const makeWithProviders = Effect.fn("makeSourceControlProviderRegistryWit...@@ -289,14 +289,14 @@ export const makeWithProviders = Effect.fn("makeSourceControlProviderRegistryWit
289 get,289 get,
290 resolveHandle,290 resolveHandle,
291 resolve: (input) => resolveHandle(input).pipe(Effect.map((handle) => handle.provider)),291 resolve: (input) => resolveHandle(input).pipe(Effect.map((handle) => handle.provider)),
292 discover: Effect.all(292 discover: Effect.forEach(
293 discoverySpecs.map((spec) =>293 discoverySpecs,
294 (spec) =>
294 probeSourceControlProvider({295 probeSourceControlProvider({
295 spec,296 spec,
296 process,297 process,
297 cwd: config.cwd,298 cwd: config.cwd,
298 }),299 }),
299 ),
300 { concurrency: "unbounded" },300 { concurrency: "unbounded" },
301 ),301 ),
302 });302 });
apps/server/src/storageCleanup.ts+3-4
...@@ -419,10 +419,9 @@ export const make = Effect.gen(function* () {...@@ -419,10 +419,9 @@ export const make = Effect.gen(function* () {
419 });419 });
420 const worker = yield* makeDrainableWorker(() =>420 const worker = yield* makeDrainableWorker(() =>
421 sweep().pipe(421 sweep().pipe(
422 Effect.catchCause((cause) =>422 Effect.catchCauseIf(
423 Cause.hasInterruptsOnly(cause)423 (cause) => !Cause.hasInterruptsOnly(cause),
424 ? Effect.failCause(cause)424 (cause) => Effect.logWarning("storage cleanup failed", { cause }),
425 : Effect.logWarning("storage cleanup failed", { cause }),
426 ),425 ),
427 ),426 ),
428 );427 );
apps/server/src/telemetry/Identify.ts+2-2
...@@ -132,7 +132,7 @@ const readIdentityFile = (...@@ -132,7 +132,7 @@ const readIdentityFile = (
132 filePath: string,132 filePath: string,
133) =>133) =>
134 fileSystem.readFileString(filePath).pipe(134 fileSystem.readFileString(filePath).pipe(
135 Effect.map(Option.some),135 Effect.asSome,
136 Effect.catchTags({136 Effect.catchTags({
137 PlatformError: (cause) =>137 PlatformError: (cause) =>
138 isNotFoundError(cause)138 isNotFoundError(cause)
...@@ -278,7 +278,7 @@ export const getTelemetryIdentifierForHome = Effect.fn("getTelemetryIdentifierFo...@@ -278,7 +278,7 @@ export const getTelemetryIdentifierForHome = Effect.fn("getTelemetryIdentifierFo
278 }278 }
279279
280 const anonymousId = yield* upsertAnonymousId.pipe(280 const anonymousId = yield* upsertAnonymousId.pipe(
281 Effect.map(Option.some),281 Effect.asSome,
282 Effect.catchTags({282 Effect.catchTags({
283 TelemetryIdentityReadError: (error) =>283 TelemetryIdentityReadError: (error) =>
284 logTelemetryIdentityError(error).pipe(Effect.as(Option.none<string>())),284 logTelemetryIdentityError(error).pipe(Effect.as(Option.none<string>())),
apps/server/src/terminal/Manager.ts+3-12
...@@ -1934,16 +1934,7 @@ export const makeWithOptions = Effect.fn("TerminalManager.makeWithOptions")(func...@@ -1934,16 +1934,7 @@ export const makeWithOptions = Effect.fn("TerminalManager.makeWithOptions")(func
1934 terminalId: string,1934 terminalId: string,
1935 ): Effect.fn.Return<TerminalSessionState, TerminalSessionLookupError> {1935 ): Effect.fn.Return<TerminalSessionState, TerminalSessionLookupError> {
1936 return yield* Effect.flatMap(getSession(threadId, terminalId), (session) =>1936 return yield* Effect.flatMap(getSession(threadId, terminalId), (session) =>
1937 Option.match(session, {1937 Effect.fromOption(session, () => new TerminalSessionLookupError({ threadId, terminalId })),
1938 onNone: () =>
1939 Effect.fail(
1940 new TerminalSessionLookupError({
1941 threadId,
1942 terminalId,
1943 }),
1944 ),
1945 onSome: Effect.succeed,
1946 }),
1947 );1938 );
1948 });1939 });
19491940
...@@ -2367,7 +2358,7 @@ export const makeWithOptions = Effect.fn("TerminalManager.makeWithOptions")(func...@@ -2367,7 +2358,7 @@ export const makeWithOptions = Effect.fn("TerminalManager.makeWithOptions")(func
2367 }2358 }
23682359
2369 const inspectorOption = yield* acquireSubprocessInspector.pipe(2360 const inspectorOption = yield* acquireSubprocessInspector.pipe(
2370 Effect.map(Option.some),2361 Effect.asSome,
2371 Effect.catch((reason) =>2362 Effect.catch((reason) =>
2372 Effect.logWarning("failed to snapshot processes for terminal subprocess polling", {2363 Effect.logWarning("failed to snapshot processes for terminal subprocess polling", {
2373 reason,2364 reason,
...@@ -2393,7 +2384,7 @@ export const makeWithOptions = Effect.fn("TerminalManager.makeWithOptions")(func...@@ -2393,7 +2384,7 @@ export const makeWithOptions = Effect.fn("TerminalManager.makeWithOptions")(func
2393 ) {2384 ) {
2394 const terminalPid = session.pid;2385 const terminalPid = session.pid;
2395 const inspectResult = yield* subprocessInspector(terminalPid).pipe(2386 const inspectResult = yield* subprocessInspector(terminalPid).pipe(
2396 Effect.map(Option.some),2387 Effect.asSome,
2397 Effect.catch((reason) =>2388 Effect.catch((reason) =>
2398 Effect.logWarning("failed to check terminal subprocess activity", {2389 Effect.logWarning("failed to check terminal subprocess activity", {
2399 threadId: session.threadId,2390 threadId: session.threadId,
apps/server/src/textGeneration/AntigravityTextGeneration.ts+1-1
...@@ -238,7 +238,7 @@ export const makeAntigravityTextGeneration = Effect.fn("makeAntigravityTextGener...@@ -238,7 +238,7 @@ export const makeAntigravityTextGeneration = Effect.fn("makeAntigravityTextGener
238 yield* applyAntigravityAcpModelSelection({238 yield* applyAntigravityAcpModelSelection({
239 runtime,239 runtime,
240 model: input.modelSelection.model,240 model: input.modelSelection.model,
241 defaultModel: yield* options.defaultModel ?? Effect.succeed(undefined),241 defaultModel: yield* options.defaultModel ?? Effect.undefined,
242 mapError: (cause) =>242 mapError: (cause) =>
243 new TextGenerationError({243 new TextGenerationError({
244 operation,244 operation,
apps/server/src/textGeneration/CodexTextGeneration.test.ts+2-2
...@@ -556,7 +556,7 @@ it.layer(CodexTextGenerationTestLayer)("CodexTextGeneration", (it) => {...@@ -556,7 +556,7 @@ it.layer(CodexTextGenerationTestLayer)("CodexTextGeneration", (it) => {
556 }),556 }),
557 ),557 ),
558 ),558 ),
559 Effect.ensuring(fs.remove(imagePath).pipe(Effect.catch(() => Effect.void))),559 Effect.ensuring(fs.remove(imagePath).pipe(Effect.ignore)),
560 );560 );
561561
562 expect(generated.branch).toBe("fix/ui-regression");562 expect(generated.branch).toBe("fix/ui-regression");
...@@ -579,7 +579,7 @@ it.layer(CodexTextGenerationTestLayer)("CodexTextGeneration", (it) => {...@@ -579,7 +579,7 @@ it.layer(CodexTextGenerationTestLayer)("CodexTextGeneration", (it) => {
579 const { attachmentsDir } = yield* ServerConfig.ServerConfig;579 const { attachmentsDir } = yield* ServerConfig.ServerConfig;
580 const missingAttachmentId = "thread-missing-attachment";580 const missingAttachmentId = "thread-missing-attachment";
581 const missingPath = path.join(attachmentsDir, `${missingAttachmentId}.png`);581 const missingPath = path.join(attachmentsDir, `${missingAttachmentId}.png`);
582 yield* fs.remove(missingPath).pipe(Effect.catch(() => Effect.void));582 yield* fs.remove(missingPath).pipe(Effect.ignore);
583583
584 const result = yield* textGeneration584 const result = yield* textGeneration
585 .generateBranchName({585 .generateBranchName({
apps/server/src/textGeneration/CodexTextGeneration.ts+4-3
...@@ -96,7 +96,7 @@ export const makeCodexTextGeneration = Effect.fn("makeCodexTextGeneration")(func...@@ -96,7 +96,7 @@ export const makeCodexTextGeneration = Effect.fn("makeCodexTextGeneration")(func
96 );96 );
9797
98 const safeUnlink = (filePath: string): Effect.Effect<void, never> =>98 const safeUnlink = (filePath: string): Effect.Effect<void, never> =>
99 fileSystem.remove(filePath).pipe(Effect.catch(() => Effect.void));99 fileSystem.remove(filePath).pipe(Effect.ignore);
100100
101 const encodeJsonForOperation = (101 const encodeJsonForOperation = (
102 operation:102 operation:
...@@ -263,8 +263,9 @@ export const makeCodexTextGeneration = Effect.fn("makeCodexTextGeneration")(func...@@ -263,8 +263,9 @@ export const makeCodexTextGeneration = Effect.fn("makeCodexTextGeneration")(func
263 }263 }
264 });264 });
265265
266 const cleanup = Effect.all(266 const cleanup = Effect.forEach(
267 [schemaPath, outputPath, ...cleanupPaths].map((filePath) => safeUnlink(filePath)),267 [schemaPath, outputPath, ...cleanupPaths],
268 (filePath) => safeUnlink(filePath),
268 {269 {
269 concurrency: "unbounded",270 concurrency: "unbounded",
270 },271 },
apps/server/src/textGeneration/ThreadTitleLinks.ts+4-7
...@@ -14,12 +14,9 @@ export const resolveThreadTitleLinks = Effect.fn("resolveThreadTitleLinks")(func...@@ -14,12 +14,9 @@ export const resolveThreadTitleLinks = Effect.fn("resolveThreadTitleLinks")(func
14 const providers = yield* SourceControlProviderRegistry.SourceControlProviderRegistry;14 const providers = yield* SourceControlProviderRegistry.SourceControlProviderRegistry;
15 const links = new Map<string, NonNullable<ReturnType<typeof providers.resolveLink>>>();15 const links = new Map<string, NonNullable<ReturnType<typeof providers.resolveLink>>>();
16 for (const match of input.message.matchAll(/https:\/\/[^\s<>"')\]`]+/g)) {16 for (const match of input.message.matchAll(/https:\/\/[^\s<>"')\]`]+/g)) {
17 let url: URL;17 const candidate = match[0].replace(/[.,;!?]+$/, "");
18 try {18 if (!URL.canParse(candidate)) continue;
19 url = new URL(match[0].replace(/[.,;!?]+$/, ""));19 const url = new URL(candidate);
20 } catch {
21 continue;
22 }
23 url.hash = "";20 url.hash = "";
24 url.search = "";21 url.search = "";
25 if (links.has(url.href)) continue;22 if (links.has(url.href)) continue;
...@@ -40,7 +37,7 @@ export const resolveThreadTitleLinks = Effect.fn("resolveThreadTitleLinks")(func...@@ -40,7 +37,7 @@ export const resolveThreadTitleLinks = Effect.fn("resolveThreadTitleLinks")(func
40 ),37 ),
41 Effect.map((summary) => `${url}\n${summary}`),38 Effect.map((summary) => `${url}\n${summary}`),
42 Effect.timeout("3 seconds"),39 Effect.timeout("3 seconds"),
43 Effect.catch(() => Effect.succeed(`${url}: unavailable`)),40 Effect.orElseSucceed(() => `${url}: unavailable`),
44 ),41 ),
45 { concurrency: 2 },42 { concurrency: 2 },
46 );43 );
apps/server/src/usage/UsageService.ts+2-2
...@@ -225,7 +225,7 @@ export const make = Effect.gen(function* () {...@@ -225,7 +225,7 @@ export const make = Effect.gen(function* () {
225225
226 yield* encodeRatesCache({ fetchedAtMs: now, document: fetched }).pipe(226 yield* encodeRatesCache({ fetchedAtMs: now, document: fetched }).pipe(
227 Effect.flatMap((serialized) => fileSystem.writeFileString(ratesCachePath, serialized)),227 Effect.flatMap((serialized) => fileSystem.writeFileString(ratesCachePath, serialized)),
228 Effect.catchCause(() => Effect.void),228 Effect.ignoreCause,
229 );229 );
230 });230 });
231231
...@@ -372,7 +372,7 @@ export const make = Effect.gen(function* () {...@@ -372,7 +372,7 @@ export const make = Effect.gen(function* () {
372 cacheDirty = false;372 cacheDirty = false;
373 }),373 }),
374 // A cache we cannot write is a slower next start, not a failed read.374 // A cache we cannot write is a slower next start, not a failed read.
375 Effect.catchCause(() => Effect.void),375 Effect.ignoreCause,
376 );376 );
377 });377 });
378378
apps/server/src/vcs/GitVcsDriver.ts+1-1
...@@ -1101,7 +1101,7 @@ export const makeVcsDriverShape = Effect.fn("makeGitVcsDriverShape")(function* (...@@ -1101,7 +1101,7 @@ export const makeVcsDriverShape = Effect.fn("makeGitVcsDriverShape")(function* (
1101 /^warning: failed to remove \.\/: [^\n]+$/.test(cleaned.stderr.trim()) &&1101 /^warning: failed to remove \.\/: [^\n]+$/.test(cleaned.stderr.trim()) &&
1102 (yield* fileSystem.readDirectory(input.cwd).pipe(1102 (yield* fileSystem.readDirectory(input.cwd).pipe(
1103 Effect.map((entries) => entries.length === 0),1103 Effect.map((entries) => entries.length === 0),
1104 Effect.catch(() => Effect.succeed(false)),1104 Effect.orElseSucceed(() => false),
1105 ));1105 ));
1106 if (!emptiedWorkspace)1106 if (!emptiedWorkspace)
1107 return yield* new VcsProcessExitError({1107 return yield* new VcsProcessExitError({
apps/server/src/vcs/GitVcsDriverCore.ts+12-17
...@@ -946,16 +946,14 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function*...@@ -946,16 +946,14 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function*
946 return yield* execution.pipe(946 return yield* execution.pipe(
947 Effect.timeoutOption(timeoutMs),947 Effect.timeoutOption(timeoutMs),
948 Effect.flatMap((result) =>948 Effect.flatMap((result) =>
949 Option.match(result, {949 Effect.fromOption(
950 onNone: () =>950 result,
951 Effect.fail(951 () =>
952 new GitCommandError({952 new GitCommandError({
953 ...gitCommandContext(commandInput),953 ...gitCommandContext(commandInput),
954 detail: "Git command timed out.",954 detail: "Git command timed out.",
955 }),955 }),
956 ),956 ),
957 onSome: Effect.succeed,
958 }),
959 ),957 ),
960 );958 );
961 },959 },
...@@ -1004,11 +1002,9 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function*...@@ -1004,11 +1002,9 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function*
1004 : {}),1002 : {}),
1005 ...(options.progress ? { progress: options.progress } : {}),1003 ...(options.progress ? { progress: options.progress } : {}),
1006 }).pipe(1004 }).pipe(
1007 Effect.flatMap((result) => {1005 Effect.filterOrFail(
1008 if (options.allowNonZeroExit || result.exitCode === 0) {1006 (result) => options.allowNonZeroExit || result.exitCode === 0,
1009 return Effect.succeed(result);1007 (result) =>
1010 }
1011 return Effect.fail(
1012 new GitCommandError({1008 new GitCommandError({
1013 ...gitCommandContext({ operation, cwd, args }),1009 ...gitCommandContext({ operation, cwd, args }),
1014 detail: options.fallbackErrorDetail ?? "Git command exited with a non-zero status.",1010 detail: options.fallbackErrorDetail ?? "Git command exited with a non-zero status.",
...@@ -1016,8 +1012,7 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function*...@@ -1016,8 +1012,7 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function*
1016 stdoutLength: result.stdout.length,1012 stdoutLength: result.stdout.length,
1017 stderrLength: result.stderr.length,1013 stderrLength: result.stderr.length,
1018 }),1014 }),
1019 );1015 ),
1020 }),
1021 );1016 );
10221017
1023 const executeGitWithStableDiagnostics = (1018 const executeGitWithStableDiagnostics = (
apps/server/src/vcs/VcsStatusBroadcaster.ts+3-3
...@@ -541,13 +541,13 @@ export const make = Effect.gen(function* () {...@@ -541,13 +541,13 @@ export const make = Effect.gen(function* () {
541 const demandCwds = yield* Ref.get(demandCwdsRef);541 const demandCwds = yield* Ref.get(demandCwdsRef);
542 const shouldRun =542 const shouldRun =
543 needsInitialRefresh ||543 needsInitialRefresh ||
544 (yield* Effect.all(544 (yield* Effect.forEach(
545 [...demandCwds.keys()].map((demandCwd) =>545 [...demandCwds.keys()],
546 (demandCwd) =>
546 backgroundPolicy.shouldRunScopeWork({547 backgroundPolicy.shouldRunScopeWork({
547 type: "vcs-status",548 type: "vcs-status",
548 cwd: demandCwd,549 cwd: demandCwd,
549 }),550 }),
550 ),
551 { concurrency: "unbounded" },551 { concurrency: "unbounded" },
552 )).some(Boolean);552 )).some(Boolean);
553 if (!shouldRun) {553 if (!shouldRun) {
apps/server/src/ws.ts+12-16
...@@ -854,27 +854,23 @@ const makeWsRpcLayer = (...@@ -854,27 +854,23 @@ const makeWsRpcLayer = (
854 case "project.meta-updated":854 case "project.meta-updated":
855 return projectUpsertOrRemove(ProjectId.make(event.aggregateId), event.sequence);855 return projectUpsertOrRemove(ProjectId.make(event.aggregateId), event.sequence);
856 case "project.deleted":856 case "project.deleted":
857 return Effect.succeed(857 return Effect.succeedSome({
858 Option.some({858 kind: "project-removed" as const,
859 kind: "project-removed" as const,859 sequence: event.sequence,
860 sequence: event.sequence,860 projectId: ProjectId.make(event.aggregateId),
861 projectId: ProjectId.make(event.aggregateId),861 });
862 }),
863 );
864 case "thread.deleted":862 case "thread.deleted":
865 case "thread.archived":863 case "thread.archived":
866 return Effect.succeed(864 return Effect.succeedSome({
867 Option.some({865 kind: "thread-removed" as const,
868 kind: "thread-removed" as const,866 sequence: event.sequence,
869 sequence: event.sequence,867 threadId: ThreadId.make(event.aggregateId),
870 threadId: ThreadId.make(event.aggregateId),868 });
871 }),
872 );
873 case "thread.unarchived":869 case "thread.unarchived":
874 return threadUpsertOrRemove(ThreadId.make(event.aggregateId), event.sequence);870 return threadUpsertOrRemove(ThreadId.make(event.aggregateId), event.sequence);
875 default:871 default:
876 if (event.aggregateKind !== "thread") {872 if (event.aggregateKind !== "thread") {
877 return Effect.succeed(Option.none());873 return Effect.succeedNone;
878 }874 }
879 return threadUpsertOrRemove(ThreadId.make(event.aggregateId), event.sequence);875 return threadUpsertOrRemove(ThreadId.make(event.aggregateId), event.sequence);
880 }876 }
...@@ -892,7 +888,7 @@ const makeWsRpcLayer = (...@@ -892,7 +888,7 @@ const makeWsRpcLayer = (
892 ): Effect.Effect<Option.Option<A>, never, never> =>888 ): Effect.Effect<Option.Option<A>, never, never> =>
893 read.pipe(889 read.pipe(
894 Effect.retry({ times: 1 }),890 Effect.retry({ times: 1 }),
895 Effect.map(Option.some),891 Effect.asSome,
896 Effect.tapError((error) =>892 Effect.tapError((error) =>
897 Effect.logWarning("orchestration shell projection refetch failed", {893 Effect.logWarning("orchestration shell projection refetch failed", {
898 aggregateKind,894 aggregateKind,
apps/web/src/connection/platform.ts+1-1
...@@ -213,7 +213,7 @@ const capabilitiesLayer = Layer.effectContext(...@@ -213,7 +213,7 @@ const capabilitiesLayer = Layer.effectContext(
213 }),213 }),
214 });214 });
215 const identity = RelayDeviceIdentity.of({215 const identity = RelayDeviceIdentity.of({
216 deviceId: Effect.succeed(Option.none()),216 deviceId: Effect.succeedNone,
217 });217 });
218 const primaryAuth = PrimaryEnvironmentAuth.of({218 const primaryAuth = PrimaryEnvironmentAuth.of({
219 bearerToken: Effect.tryPromise({219 bearerToken: Effect.tryPromise({
apps/web/src/connection/storage.ts+4-4
...@@ -587,7 +587,7 @@ export const connectionStorageLayer = Layer.effectContext(...@@ -587,7 +587,7 @@ export const connectionStorageLayer = Layer.effectContext(
587 Effect.tap(() => Effect.promise(() => projectFaviconCache.hydrate())),587 Effect.tap(() => Effect.promise(() => projectFaviconCache.hydrate())),
588 Effect.flatMap((raw) => {588 Effect.flatMap((raw) => {
589 if (typeof raw !== "string") {589 if (typeof raw !== "string") {
590 return Effect.succeed(Option.none());590 return Effect.succeedNone;
591 }591 }
592 return decodeStoredShellSnapshot(raw).pipe(592 return decodeStoredShellSnapshot(raw).pipe(
593 Effect.mapError((cause) => persistenceError("load-shell", cause)),593 Effect.mapError((cause) => persistenceError("load-shell", cause)),
...@@ -623,7 +623,7 @@ export const connectionStorageLayer = Layer.effectContext(...@@ -623,7 +623,7 @@ export const connectionStorageLayer = Layer.effectContext(
623 readDatabaseValue(database, SERVER_CONFIG_STORE_NAME, environmentId).pipe(623 readDatabaseValue(database, SERVER_CONFIG_STORE_NAME, environmentId).pipe(
624 Effect.flatMap((raw) => {624 Effect.flatMap((raw) => {
625 if (typeof raw !== "string") {625 if (typeof raw !== "string") {
626 return Effect.succeed(Option.none());626 return Effect.succeedNone;
627 }627 }
628 return decodeStoredServerConfig(raw).pipe(628 return decodeStoredServerConfig(raw).pipe(
629 Effect.mapError((cause) => persistenceError("load-server-config", cause)),629 Effect.mapError((cause) => persistenceError("load-server-config", cause)),
...@@ -661,7 +661,7 @@ export const connectionStorageLayer = Layer.effectContext(...@@ -661,7 +661,7 @@ export const connectionStorageLayer = Layer.effectContext(
661 ).pipe(661 ).pipe(
662 Effect.flatMap((raw) => {662 Effect.flatMap((raw) => {
663 if (typeof raw !== "string") {663 if (typeof raw !== "string") {
664 return Effect.succeed(Option.none());664 return Effect.succeedNone;
665 }665 }
666 return decodeStoredThreadSnapshot(raw).pipe(666 return decodeStoredThreadSnapshot(raw).pipe(
667 Effect.mapError((cause) => persistenceError("load-thread", cause)),667 Effect.mapError((cause) => persistenceError("load-thread", cause)),
...@@ -703,7 +703,7 @@ export const connectionStorageLayer = Layer.effectContext(...@@ -703,7 +703,7 @@ export const connectionStorageLayer = Layer.effectContext(
703 readDatabaseValue(database, VCS_REFS_STORE_NAME, vcsRefsCacheKey(environmentId, cwd)).pipe(703 readDatabaseValue(database, VCS_REFS_STORE_NAME, vcsRefsCacheKey(environmentId, cwd)).pipe(
704 Effect.flatMap((raw) => {704 Effect.flatMap((raw) => {
705 if (typeof raw !== "string") {705 if (typeof raw !== "string") {
706 return Effect.succeed(Option.none());706 return Effect.succeedNone;
707 }707 }
708 return decodeStoredVcsRefs(raw).pipe(708 return decodeStoredVcsRefs(raw).pipe(
709 Effect.mapError((cause) => persistenceError("load-vcs-refs", cause)),709 Effect.mapError((cause) => persistenceError("load-vcs-refs", cause)),
infra/relay/src/agentActivity/LiveActivities.test.ts+3-2
...@@ -276,7 +276,7 @@ describe("LiveActivities", () => {...@@ -276,7 +276,7 @@ describe("LiveActivities", () => {
276 liveActivities.register({ userId: "user-1", registration }),276 liveActivities.register({ userId: "user-1", registration }),
277 );277 );
278 const targetListError = yield* Effect.flip(liveActivities.listTargets({ userId: "user-1" }));278 const targetListError = yield* Effect.flip(liveActivities.listTargets({ userId: "user-1" }));
279 const deliveryErrors = yield* Effect.all(279 const deliveryErrors = yield* Effect.forEach(
280 [280 [
281 liveActivities.markDelivery({281 liveActivities.markDelivery({
282 userId: "user-1",282 userId: "user-1",
...@@ -297,7 +297,8 @@ describe("LiveActivities", () => {...@@ -297,7 +297,8 @@ describe("LiveActivities", () => {
297 kind: "push_notification",297 kind: "push_notification",
298 invalidatedAt: "2026-05-25T00:00:10.000Z",298 invalidatedAt: "2026-05-25T00:00:10.000Z",
299 }),299 }),
300 ].map(Effect.flip),300 ],
301 (effect) => Effect.flip(effect),
301 { concurrency: 1 },302 { concurrency: 1 },
302 );303 );
303304
infra/relay/src/clientConfig.test.ts+7-31
...@@ -82,13 +82,7 @@ describe("PublishClientConfig", () => {...@@ -82,13 +82,7 @@ describe("PublishClientConfig", () => {
82 ),82 ),
83 );83 );
8484
85 yield* stack85 yield* stack.deploy(PublishClientConfig(clientConfig("v1"))).pipe(configured);
86 .deploy(
87 Effect.gen(function* () {
88 return yield* PublishClientConfig(clientConfig("v1"));
89 }),
90 )
91 .pipe(configured);
92 const first = yield* fs.readFileString(target);86 const first = yield* fs.readFileString(target);
93 expect(first).toContain("KEEP=yes\n");87 expect(first).toContain("KEEP=yes\n");
94 expect(first).toContain("T3CODE_RELAY_URL=https://relay.example.com\n");88 expect(first).toContain("T3CODE_RELAY_URL=https://relay.example.com\n");
...@@ -97,23 +91,11 @@ describe("PublishClientConfig", () => {...@@ -97,23 +91,11 @@ describe("PublishClientConfig", () => {
9791
98 // Same input: the action is skipped, so a change made by hand survives.92 // Same input: the action is skipped, so a change made by hand survives.
99 yield* fs.writeFileString(target, `${first}MANUAL=1\n`);93 yield* fs.writeFileString(target, `${first}MANUAL=1\n`);
100 yield* stack94 yield* stack.deploy(PublishClientConfig(clientConfig("v1"))).pipe(configured);
101 .deploy(
102 Effect.gen(function* () {
103 return yield* PublishClientConfig(clientConfig("v1"));
104 }),
105 )
106 .pipe(configured);
107 expect(yield* fs.readFileString(target)).toContain("MANUAL=1\n");95 expect(yield* fs.readFileString(target)).toContain("MANUAL=1\n");
10896
109 // A rotated token changes the input, so it runs again and replaces the line.97 // A rotated token changes the input, so it runs again and replaces the line.
110 yield* stack98 yield* stack.deploy(PublishClientConfig(clientConfig("v2"))).pipe(configured);
111 .deploy(
112 Effect.gen(function* () {
113 return yield* PublishClientConfig(clientConfig("v2"));
114 }),
115 )
116 .pipe(configured);
117 const third = yield* fs.readFileString(target);99 const third = yield* fs.readFileString(target);
118 expect(third).toContain("T3CODE_RELAY_CLIENT_OTLP_TRACES_TOKEN=client-v2\n");100 expect(third).toContain("T3CODE_RELAY_CLIENT_OTLP_TRACES_TOKEN=client-v2\n");
119 expect(third).not.toContain("client-v1");101 expect(third).not.toContain("client-v1");
...@@ -128,11 +110,7 @@ describe("PublishClientConfig", () => {...@@ -128,11 +110,7 @@ describe("PublishClientConfig", () => {
128 const dir = yield* fs.makeTempDirectoryScoped({ prefix: "t3-relay-client-config-" });110 const dir = yield* fs.makeTempDirectoryScoped({ prefix: "t3-relay-client-config-" });
129 const target = path.join(dir, "client.env");111 const target = path.join(dir, "client.env");
130 const exit = yield* stack112 const exit = yield* stack
131 .deploy(113 .deploy(PublishClientConfig({ ...clientConfig("v1"), url: undefined }))
132 Effect.gen(function* () {
133 return yield* PublishClientConfig({ ...clientConfig("v1"), url: undefined });
134 }),
135 )
136 .pipe(114 .pipe(
137 Effect.provide(115 Effect.provide(
138 ConfigProvider.layer(116 ConfigProvider.layer(
...@@ -155,11 +133,9 @@ describe("PublishClientConfig", () => {...@@ -155,11 +133,9 @@ describe("PublishClientConfig", () => {
155 const target = path.join(dir, "client.env");133 const target = path.join(dir, "client.env");
156 const exit = yield* stack134 const exit = yield* stack
157 .deploy(135 .deploy(
158 Effect.gen(function* () {136 PublishClientConfig({
159 return yield* PublishClientConfig({137 ...clientConfig("v1"),
160 ...clientConfig("v1"),138 clientTracingDataset: "relay-traces\nINJECTED=1",
161 clientTracingDataset: "relay-traces\nINJECTED=1",
162 });
163 }),139 }),
164 )140 )
165 .pipe(141 .pipe(
infra/relay/src/environments/ManagedEndpointAllocations.ts+1-3
...@@ -567,9 +567,7 @@ export const make = Effect.gen(function* () {...@@ -567,9 +567,7 @@ export const make = Effect.gen(function* () {
567 .pipe(567 .pipe(
568 Effect.mapError(lockError),568 Effect.mapError(lockError),
569 Effect.flatMap((rows) =>569 Effect.flatMap((rows) =>
570 rows.length === 0570 rows.length === 0 ? Effect.succeedNone : Effect.asSome(effect),
571 ? Effect.succeed(Option.none())
572 : effect.pipe(Effect.map(Option.some)),
573 ),571 ),
574 ),572 ),
575 )573 )
infra/relay/src/environments/ManagedEndpointProvider.test.ts+5-8
...@@ -6,7 +6,6 @@ import * as Alchemy from "alchemy";...@@ -6,7 +6,6 @@ import * as Alchemy from "alchemy";
6import * as Cloudflare from "alchemy/Cloudflare";6import * as Cloudflare from "alchemy/Cloudflare";
7import * as Effect from "effect/Effect";7import * as Effect from "effect/Effect";
8import * as Layer from "effect/Layer";8import * as Layer from "effect/Layer";
9import * as Option from "effect/Option";
10import * as Redacted from "effect/Redacted";9import * as Redacted from "effect/Redacted";
1110
12import * as RelayConfiguration from "../Config.ts";11import * as RelayConfiguration from "../Config.ts";
...@@ -302,8 +301,8 @@ function makeAllocations(calls: AllocationCall[] = []) {...@@ -302,8 +301,8 @@ function makeAllocations(calls: AllocationCall[] = []) {
302 Effect.suspend(() => {301 Effect.suspend(() => {
303 const current = allocations.get(allocationKey(input));302 const current = allocations.get(allocationKey(input));
304 return current?.tunnelId === input.tunnelId && current.generation === input.generation303 return current?.tunnelId === input.tunnelId && current.generation === input.generation
305 ? effect.pipe(Effect.map(Option.some))304 ? Effect.asSome(effect)
306 : Effect.succeed(Option.none());305 : Effect.succeedNone;
307 }),306 }),
308 claimDeprovision: (input) =>307 claimDeprovision: (input) =>
309 Effect.sync(() => {308 Effect.sync(() => {
...@@ -1189,7 +1188,7 @@ describe("ManagedEndpointProvider", () => {...@@ -1189,7 +1188,7 @@ describe("ManagedEndpointProvider", () => {
1189 const changed = ManagedEndpointAllocations.ManagedEndpointAllocations.of({1188 const changed = ManagedEndpointAllocations.ManagedEndpointAllocations.of({
1190 ...allocations,1189 ...allocations,
1191 withClaimedTunnel: (input, effect) =>1190 withClaimedTunnel: (input, effect) =>
1192 loseClaim ? Effect.succeed(Option.none()) : allocations.withClaimedTunnel(input, effect),1191 loseClaim ? Effect.succeedNone : allocations.withClaimedTunnel(input, effect),
1193 });1192 });
1194 const layer = providerLayer(makePersistentTunnelClient(), makeDnsClient(), changed);1193 const layer = providerLayer(makePersistentTunnelClient(), makeDnsClient(), changed);
11951194
...@@ -1320,7 +1319,7 @@ describe("ManagedEndpointProvider", () => {...@@ -1320,7 +1319,7 @@ describe("ManagedEndpointProvider", () => {
1320 const allocations = makeAllocations();1319 const allocations = makeAllocations();
1321 const changed = ManagedEndpointAllocations.ManagedEndpointAllocations.of({1320 const changed = ManagedEndpointAllocations.ManagedEndpointAllocations.of({
1322 ...allocations,1321 ...allocations,
1323 withClaimedTunnel: () => Effect.succeed(Option.none()),1322 withClaimedTunnel: () => Effect.succeedNone,
1324 });1323 });
1325 const layer = providerLayer(makePersistentTunnelClient(tunnelCalls), makeDnsClient(), changed);1324 const layer = providerLayer(makePersistentTunnelClient(tunnelCalls), makeDnsClient(), changed);
13261325
...@@ -1349,9 +1348,7 @@ describe("ManagedEndpointProvider", () => {...@@ -1349,9 +1348,7 @@ describe("ManagedEndpointProvider", () => {
1349 const changed = ManagedEndpointAllocations.ManagedEndpointAllocations.of({1348 const changed = ManagedEndpointAllocations.ManagedEndpointAllocations.of({
1350 ...allocations,1349 ...allocations,
1351 withClaimedTunnel: (input, effect) =>1350 withClaimedTunnel: (input, effect) =>
1352 ++lockCount === 11351 ++lockCount === 1 ? allocations.withClaimedTunnel(input, effect) : Effect.succeedNone,
1353 ? allocations.withClaimedTunnel(input, effect)
1354 : Effect.succeed(Option.none()),
1355 });1352 });
1356 const layer = providerLayer(makePersistentTunnelClient(), makeDnsClient(dnsCalls), changed);1353 const layer = providerLayer(makePersistentTunnelClient(), makeDnsClient(dnsCalls), changed);
13571354
infra/relay/src/environments/ManagedEndpointProvider.ts+17-16
...@@ -471,8 +471,9 @@ export const make = Effect.gen(function* () {...@@ -471,8 +471,9 @@ export const make = Effect.gen(function* () {
471 ? updateExistingDnsRecords(records, preferredDnsRecordId, dnsRecord)471 ? updateExistingDnsRecords(records, preferredDnsRecordId, dnsRecord)
472 : Effect.fail(createError),472 : Effect.fail(createError),
473 ),473 ),
474 Effect.flatMap((dnsRecordId) =>474 Effect.filterOrFail(
475 dnsRecordId === null ? Effect.fail(createError) : Effect.succeed(dnsRecordId),475 (dnsRecordId) => dnsRecordId !== null,
476 () => createError,
476 ),477 ),
477 ),478 ),
478 }),479 }),
...@@ -578,19 +579,19 @@ export const make = Effect.gen(function* () {...@@ -578,19 +579,19 @@ export const make = Effect.gen(function* () {
578 ? Effect.succeed("missing" as const)579 ? Effect.succeed("missing" as const)
579 : Effect.fail(error),580 : Effect.fail(error),
580 }),581 }),
581 Effect.flatMap((result) =>582 Effect.filterOrElse(
582 result === "missing"583 (result): result is "missing" => result === "missing",
583 ? Effect.succeed(result)584 () =>
584 : allocations585 allocations
585 .markReady({586 .markReady({
586 ...input,587 ...input,
587 generation: allocation.generation,588 generation: allocation.generation,
588 })589 })
589 .pipe(590 .pipe(
590 Effect.map((updated) =>591 Effect.map((updated) =>
591 updated ? ("configured" as const) : ("stale" as const),592 updated ? ("configured" as const) : ("stale" as const),
592 ),
593 ),593 ),
594 ),
594 ),595 ),
595 ),596 ),
596 )597 )
...@@ -791,11 +792,11 @@ export const make = Effect.gen(function* () {...@@ -791,11 +792,11 @@ export const make = Effect.gen(function* () {
791 const expectedStatus = input.expectedStatus;792 const expectedStatus = input.expectedStatus;
792 const inactiveBefore = input.expectedInactiveBefore;793 const inactiveBefore = input.expectedInactiveBefore;
793 const currentTunnel = yield* tunnels.get(tunnelId).pipe(794 const currentTunnel = yield* tunnels.get(tunnelId).pipe(
794 Effect.map(Option.some),795 Effect.asSome,
795 Effect.catchTags({796 Effect.catchTags({
796 ManagedEndpointTunnelClientError: (cause) =>797 ManagedEndpointTunnelClientError: (cause) =>
797 isManagedEndpointNotFound(cause.cause)798 isManagedEndpointNotFound(cause.cause)
798 ? Effect.succeed(Option.none())799 ? Effect.succeedNone
799 : Effect.fail(800 : Effect.fail(
800 new ManagedEndpointDeprovisioningFailed({801 new ManagedEndpointDeprovisioningFailed({
801 ...input,802 ...input,
packages/client-runtime/src/authorization/layer.test.ts+1-1
...@@ -199,7 +199,7 @@ const makeHarness = Effect.fn("TestRemoteAuthorization.makeHarness")(function* (...@@ -199,7 +199,7 @@ const makeHarness = Effect.fn("TestRemoteAuthorization.makeHarness")(function* (
199 clerkToken: input.clerkToken ?? Effect.succeed("clerk-session"),199 clerkToken: input.clerkToken ?? Effect.succeed("clerk-session"),
200 }),200 }),
201 Layer.succeed(ClientCapabilities.RelayDeviceIdentity, {201 Layer.succeed(ClientCapabilities.RelayDeviceIdentity, {
202 deviceId: Effect.succeed(Option.some("device-1")),202 deviceId: Effect.succeedSome("device-1"),
203 }),203 }),
204 Layer.succeed(TokenStore.RemoteDpopAccessTokenStore, tokenStore),204 Layer.succeed(TokenStore.RemoteDpopAccessTokenStore, tokenStore),
205 Layer.succeed(205 Layer.succeed(
packages/client-runtime/src/connection/registry.test.ts+3-3
...@@ -275,12 +275,12 @@ const makeHarness = Effect.fn("TestEnvironmentRegistry.makeHarness")(function* (...@@ -275,12 +275,12 @@ const makeHarness = Effect.fn("TestEnvironmentRegistry.makeHarness")(function* (
275 next.set(environmentId, snapshot);275 next.set(environmentId, snapshot);
276 return next;276 return next;
277 }),277 }),
278 loadThread: (_environmentId, _threadId) => Effect.succeed(Option.none()),278 loadThread: (_environmentId, _threadId) => Effect.succeedNone,
279 saveThread: (_environmentId, _thread) => Effect.void,279 saveThread: (_environmentId, _thread) => Effect.void,
280 removeThread: (_environmentId, _threadId) => Effect.void,280 removeThread: (_environmentId, _threadId) => Effect.void,
281 loadServerConfig: () => Effect.succeed(Option.none()),281 loadServerConfig: () => Effect.succeedNone,
282 saveServerConfig: () => Effect.void,282 saveServerConfig: () => Effect.void,
283 loadVcsRefs: () => Effect.succeed(Option.none()),283 loadVcsRefs: () => Effect.succeedNone,
284 saveVcsRefs: () => Effect.void,284 saveVcsRefs: () => Effect.void,
285 removeVcsRefs: () => Effect.void,285 removeVcsRefs: () => Effect.void,
286 clearVcsRefs: () => Effect.void,286 clearVcsRefs: () => Effect.void,
packages/client-runtime/src/connection/resolver.ts+6-8
...@@ -110,10 +110,9 @@ const makeBearerBroker = Effect.fn("clientRuntime.connection.broker.makeBearer")...@@ -110,10 +110,9 @@ const makeBearerBroker = Effect.fn("clientRuntime.connection.broker.makeBearer")
110 entry: ConnectionCatalogEntry & { readonly target: BearerConnectionTarget },110 entry: ConnectionCatalogEntry & { readonly target: BearerConnectionTarget },
111 ) {111 ) {
112 const target = entry.target;112 const target = entry.target;
113 const profile = yield* Option.match(entry.profile, {113 const profile = yield* Effect.fromOption(entry.profile, () =>
114 onNone: () => Effect.fail(profileMissingError(target.connectionId)),114 profileMissingError(target.connectionId),
115 onSome: Effect.succeed,115 );
116 });
117 if (!isBearerProfile(profile)) {116 if (!isBearerProfile(profile)) {
118 return yield* new ConnectionBlockedError({117 return yield* new ConnectionBlockedError({
119 reason: "configuration",118 reason: "configuration",
...@@ -186,10 +185,9 @@ const makeSshBroker = Effect.fn("clientRuntime.connection.broker.makeSsh")(funct...@@ -186,10 +185,9 @@ const makeSshBroker = Effect.fn("clientRuntime.connection.broker.makeSsh")(funct
186 entry: ConnectionCatalogEntry & { readonly target: SshConnectionTarget },185 entry: ConnectionCatalogEntry & { readonly target: SshConnectionTarget },
187 ) {186 ) {
188 const target = entry.target;187 const target = entry.target;
189 const profile = yield* Option.match(entry.profile, {188 const profile = yield* Effect.fromOption(entry.profile, () =>
190 onNone: () => Effect.fail(profileMissingError(target.connectionId)),189 profileMissingError(target.connectionId),
191 onSome: Effect.succeed,190 );
192 });
193 if (!isSshProfile(profile)) {191 if (!isSshProfile(profile)) {
194 return yield* new ConnectionBlockedError({192 return yield* new ConnectionBlockedError({
195 reason: "configuration",193 reason: "configuration",
packages/client-runtime/src/connection/supervisor.test.ts+2-2
...@@ -1290,11 +1290,11 @@ describe("EnvironmentSupervisor", () => {...@@ -1290,11 +1290,11 @@ describe("EnvironmentSupervisor", () => {
1290 Layer.succeed(ManagedRelayDpopSigner, signer),1290 Layer.succeed(ManagedRelayDpopSigner, signer),
1291 Layer.succeed(ManagedRelayClient, relay),1291 Layer.succeed(ManagedRelayClient, relay),
1292 Layer.succeed(ClientCapabilities.CloudSession, {1292 Layer.succeed(ClientCapabilities.CloudSession, {
1293 identity: Effect.succeed(Option.some({ accountId: "test-account" })),1293 identity: Effect.succeedSome({ accountId: "test-account" }),
1294 clerkToken: Effect.succeed("clerk-token"),1294 clerkToken: Effect.succeed("clerk-token"),
1295 }),1295 }),
1296 Layer.succeed(ClientCapabilities.RelayDeviceIdentity, {1296 Layer.succeed(ClientCapabilities.RelayDeviceIdentity, {
1297 deviceId: Effect.succeed(Option.none()),1297 deviceId: Effect.succeedNone,
1298 }),1298 }),
1299 TokenStore.layer({1299 TokenStore.layer({
1300 get: () => Ref.get(token),1300 get: () => Ref.get(token),
packages/client-runtime/src/connection/supervisor.ts+1
...@@ -606,6 +606,7 @@ export const make = Effect.fn("EnvironmentSupervisor.make")(function* (...@@ -606,6 +606,7 @@ export const make = Effect.fn("EnvironmentSupervisor.make")(function* (
606 }, Effect.ensuring(clearLease));606 }, Effect.ensuring(clearLease));
607607
608 const waitForRetrySignal = Effect.fnUntraced(function* (delayMs: number) {608 const waitForRetrySignal = Effect.fnUntraced(function* (delayMs: number) {
609 // @effect-diagnostics-next-line raceFirstWithSleepToTimeout:off - the sleep is the retry delay (false), not a timeout around the signal loop
609 return yield* Effect.raceFirst(610 return yield* Effect.raceFirst(
610 Effect.sleep(delayMs).pipe(Effect.as(false)),611 Effect.sleep(delayMs).pipe(Effect.as(false)),
611 Effect.gen(function* () {612 Effect.gen(function* () {
packages/client-runtime/src/platform/storageDocument.test.ts+1-1
...@@ -207,7 +207,7 @@ describe("ConnectionCatalogDocument", () => {...@@ -207,7 +207,7 @@ describe("ConnectionCatalogDocument", () => {
207 remoteDpopTokens: [token],207 remoteDpopTokens: [token],
208 };208 };
209 const schema = Schema.fromJsonString(ConnectionCatalogDocument);209 const schema = Schema.fromJsonString(ConnectionCatalogDocument);
210 const restored = Schema.decodeUnknownSync(schema)(Schema.encodeSync(schema)(document));210 const restored = Schema.decodeSync(schema)(Schema.encodeSync(schema)(document));
211211
212 expect(restored).toEqual(document);212 expect(restored).toEqual(document);
213 expect(restored.remoteDpopTokens[0]?.accountId).toBe(accountId);213 expect(restored.remoteDpopTokens[0]?.accountId).toBe(accountId);
packages/client-runtime/src/relay/discovery.test.ts+8-10
...@@ -134,15 +134,13 @@ const makeHarness = Effect.fn("RelayDiscoveryTest.makeHarness")(function* () {...@@ -134,15 +134,13 @@ const makeHarness = Effect.fn("RelayDiscoveryTest.makeHarness")(function* () {
134 ),134 ),
135 ),135 ),
136 clerkToken: Ref.get(clerkToken).pipe(136 clerkToken: Ref.get(clerkToken).pipe(
137 Effect.flatMap((token) =>137 Effect.filterOrFail(
138 token === null138 (token) => token !== null,
139 ? Effect.fail(139 () =>
140 new ConnectionBlockedError({140 new ConnectionBlockedError({
141 reason: "authentication",141 reason: "authentication",
142 detail: "Signed out.",142 detail: "Signed out.",
143 }),143 }),
144 )
145 : Effect.succeed(token),
146 ),144 ),
147 ),145 ),
148 }),146 }),
...@@ -289,7 +287,7 @@ describe("RelayEnvironmentDiscovery", () => {...@@ -289,7 +287,7 @@ describe("RelayEnvironmentDiscovery", () => {
289 Layer.mergeAll(287 Layer.mergeAll(
290 Layer.succeed(ManagedRelay.ManagedRelayClient, client),288 Layer.succeed(ManagedRelay.ManagedRelayClient, client),
291 Layer.succeed(ClientCapabilities.CloudSession, {289 Layer.succeed(ClientCapabilities.CloudSession, {
292 identity: Effect.succeed(Option.some({ accountId: "account-1" })),290 identity: Effect.succeedSome({ accountId: "account-1" }),
293 clerkToken: Effect.succeed("clerk-token"),291 clerkToken: Effect.succeed("clerk-token"),
294 }),292 }),
295 Layer.succeed(Connectivity.Connectivity, {293 Layer.succeed(Connectivity.Connectivity, {
packages/client-runtime/src/relay/discovery.ts+1-1
...@@ -241,7 +241,7 @@ export const make = Effect.fn("RelayEnvironmentDiscovery.make")(function* () {...@@ -241,7 +241,7 @@ export const make = Effect.fn("RelayEnvironmentDiscovery.make")(function* () {
241 }));241 }));
242 return;242 return;
243 }243 }
244 return yield* Effect.fail(failure);244 return yield* failure;
245 }245 }
246 const clerkToken = tokenResult.success;246 const clerkToken = tokenResult.success;
247 if ((yield* Ref.get(accountGeneration)) !== generation) {247 if ((yield* Ref.get(accountGeneration)) !== generation) {
packages/client-runtime/src/relay/managedRelay.ts+4-7
...@@ -666,11 +666,8 @@ export const make = Effect.fn("ManagedRelayClient.make")(function* (...@@ -666,11 +666,8 @@ export const make = Effect.fn("ManagedRelayClient.make")(function* (
666 authorize(input).pipe(666 authorize(input).pipe(
667 Effect.flatMap((authorization) =>667 Effect.flatMap((authorization) =>
668 request(authorization).pipe(668 request(authorization).pipe(
669 Effect.catch((error) => {669 Effect.catchIf(isRejectedDpopAccessToken, (error) =>
670 if (!isRejectedDpopAccessToken(error)) {670 invalidateAccessToken(authorization.accessToken).pipe(
671 return Effect.fail(error);
672 }
673 return invalidateAccessToken(authorization.accessToken).pipe(
674 Effect.tap((invalidated) =>671 Effect.tap((invalidated) =>
675 Effect.annotateCurrentSpan({672 Effect.annotateCurrentSpan({
676 "relay.token_cache.invalidated": invalidated,673 "relay.token_cache.invalidated": invalidated,
...@@ -686,8 +683,8 @@ export const make = Effect.fn("ManagedRelayClient.make")(function* (...@@ -686,8 +683,8 @@ export const make = Effect.fn("ManagedRelayClient.make")(function* (
686 : Effect.void,683 : Effect.void,
687 ),684 ),
688 Effect.andThen(refreshRejectedToken ? attempt(false) : Effect.fail(error)),685 Effect.andThen(refreshRejectedToken ? attempt(false) : Effect.fail(error)),
689 );686 ),
690 }),687 ),
691 ),688 ),
692 ),689 ),
693 );690 );
packages/client-runtime/src/relay/managedRelayState.ts+6-8
...@@ -180,14 +180,12 @@ function readSessionClerkToken(...@@ -180,14 +180,12 @@ function readSessionClerkToken(
180 session: ManagedRelaySession,180 session: ManagedRelaySession,
181): Effect.Effect<string, ManagedRelaySessionError> {181): Effect.Effect<string, ManagedRelaySessionError> {
182 return session.readClerkToken().pipe(182 return session.readClerkToken().pipe(
183 Effect.flatMap((token) =>183 Effect.filterOrFail(
184 token184 (token): token is string => Boolean(token),
185 ? Effect.succeed(token)185 () =>
186 : Effect.fail(186 new ManagedRelaySessionError({
187 new ManagedRelaySessionError({187 message: "The T3 Connect session token is unavailable.",
188 message: "The T3 Connect session token is unavailable.",188 }),
189 }),
190 ),
191 ),189 ),
192 );190 );
193}191}
packages/client-runtime/src/rpc/session.test.ts+5-5
...@@ -161,7 +161,7 @@ const RpcRequest = Schema.TaggedStruct("Request", {...@@ -161,7 +161,7 @@ const RpcRequest = Schema.TaggedStruct("Request", {
161});161});
162const decodeJson = Schema.decodeUnknownSync(Schema.fromJsonString(Schema.Unknown));162const decodeJson = Schema.decodeUnknownSync(Schema.fromJsonString(Schema.Unknown));
163const isRpcRequest = Schema.is(RpcRequest);163const isRpcRequest = Schema.is(RpcRequest);
164const isPing = Schema.is(Schema.Struct({ _tag: Schema.Literal("Ping") }));164const isPing = Schema.is(Schema.TaggedStruct("Ping", {}));
165const encodeJson = Schema.encodeUnknownSync(Schema.fromJsonString(Schema.Unknown));165const encodeJson = Schema.encodeUnknownSync(Schema.fromJsonString(Schema.Unknown));
166const encodeServerConfig = Schema.encodeSync(ServerConfig);166const encodeServerConfig = Schema.encodeSync(ServerConfig);
167const encodeServerConfigStreamEvent = Schema.encodeSync(ServerConfigStreamEvent);167const encodeServerConfigStreamEvent = Schema.encodeSync(ServerConfigStreamEvent);
...@@ -823,14 +823,14 @@ describe("RpcSessionFactory", () => {...@@ -823,14 +823,14 @@ describe("RpcSessionFactory", () => {
823 retryNow: Effect.void,823 retryNow: Effect.void,
824 } satisfies EnvironmentSupervisor.EnvironmentSupervisor["Service"]);824 } satisfies EnvironmentSupervisor.EnvironmentSupervisor["Service"]);
825 const cache = Persistence.EnvironmentCacheStore.of({825 const cache = Persistence.EnvironmentCacheStore.of({
826 loadShell: () => Effect.succeed(Option.none()),826 loadShell: () => Effect.succeedNone,
827 saveShell: () => Effect.void,827 saveShell: () => Effect.void,
828 loadThread: () => Effect.succeed(Option.none()),828 loadThread: () => Effect.succeedNone,
829 saveThread: () => Effect.void,829 saveThread: () => Effect.void,
830 removeThread: () => Effect.void,830 removeThread: () => Effect.void,
831 loadServerConfig: () => Effect.succeed(Option.none()),831 loadServerConfig: () => Effect.succeedNone,
832 saveServerConfig: () => Effect.void,832 saveServerConfig: () => Effect.void,
833 loadVcsRefs: () => Effect.succeed(Option.none()),833 loadVcsRefs: () => Effect.succeedNone,
834 saveVcsRefs: () => Effect.void,834 saveVcsRefs: () => Effect.void,
835 removeVcsRefs: () => Effect.void,835 removeVcsRefs: () => Effect.void,
836 clearVcsRefs: () => Effect.void,836 clearVcsRefs: () => Effect.void,
packages/client-runtime/src/rpc/session.ts+7-7
...@@ -291,13 +291,13 @@ export const make = Effect.fn("RpcSessionFactory.make")(function* (...@@ -291,13 +291,13 @@ export const make = Effect.fn("RpcSessionFactory.make")(function* (
291 Effect.flatMap(() => Effect.fail(configSubscriptionEndedError)),291 Effect.flatMap(() => Effect.fail(configSubscriptionEndedError)),
292 ),292 ),
293 ).pipe(293 ).pipe(
294 Effect.flatMap((config) =>294 Effect.filterOrElse(
295 config.environment.environmentId === connection.environmentId295 (config) => config.environment.environmentId === connection.environmentId,
296 ? Effect.succeed(config)296 (config) =>
297 : environmentMismatchError({297 environmentMismatchError({
298 expected: connection.environmentId,298 expected: connection.environmentId,
299 actual: config.environment.environmentId,299 actual: config.environment.environmentId,
300 }),300 }),
301 ),301 ),
302 Effect.withSpan("environment.initialSync"),302 Effect.withSpan("environment.initialSync"),
303 );303 );
packages/client-runtime/src/state/pullRequestRouting.ts+5-9
...@@ -339,15 +339,11 @@ export function createPullRequestRouter() {...@@ -339,15 +339,11 @@ export function createPullRequestRouter() {
339 }339 }
340 const operation = run(id);340 const operation = run(id);
341 return yield* (reads.has(tag) ? operation.pipe(readTimeout(id)) : operation).pipe(341 return yield* (reads.has(tag) ? operation.pipe(readTimeout(id)) : operation).pipe(
342 Effect.catch((error) => {342 Effect.catchIf(
343 if (343 (error) =>
344 (reads.has(tag) || rejectedBeforeDispatch(error)) &&344 (reads.has(tag) || rejectedBeforeDispatch(error)) && index + 1 < candidates.length,
345 index + 1 < candidates.length345 () => visit(index + 1),
346 ) {346 ),
347 return visit(index + 1);
348 }
349 return Effect.fail(error);
350 }),
351 Effect.map((result) =>347 Effect.map((result) =>
352 typeof result === "object" && result !== null && "projectId" in result348 typeof result === "object" && result !== null && "projectId" in result
353 ? {349 ? {
packages/client-runtime/src/state/pullRequests.test.ts+1-1
...@@ -1171,7 +1171,7 @@ it.effect("updates cached labels after successful edits without rereading the ho...@@ -1171,7 +1171,7 @@ it.effect("updates cached labels after successful edits without rereading the ho
1171 if (failDetail) {1171 if (failDetail) {
1172 yield* detailRefreshStarted.open;1172 yield* detailRefreshStarted.open;
1173 yield* releaseDetailRefresh.await;1173 yield* releaseDetailRefresh.await;
1174 return yield* Effect.fail(new MutationRefused());1174 return yield* new MutationRefused();
1175 }1175 }
1176 return { title: "keep this title", labels: [existing] };1176 return { title: "keep this title", labels: [existing] };
1177 }),1177 }),
packages/client-runtime/src/state/server.test.ts+8-8
...@@ -778,14 +778,14 @@ describe("server state projection", () => {...@@ -778,14 +778,14 @@ describe("server state projection", () => {
778 } satisfies EnvironmentSupervisor.EnvironmentSupervisor["Service"]);778 } satisfies EnvironmentSupervisor.EnvironmentSupervisor["Service"]);
779 const savedConfigs = yield* Queue.unbounded<ServerConfig>();779 const savedConfigs = yield* Queue.unbounded<ServerConfig>();
780 const cache = Persistence.EnvironmentCacheStore.of({780 const cache = Persistence.EnvironmentCacheStore.of({
781 loadShell: () => Effect.succeed(Option.none()),781 loadShell: () => Effect.succeedNone,
782 saveShell: () => Effect.void,782 saveShell: () => Effect.void,
783 loadThread: () => Effect.succeed(Option.none()),783 loadThread: () => Effect.succeedNone,
784 saveThread: () => Effect.void,784 saveThread: () => Effect.void,
785 removeThread: () => Effect.void,785 removeThread: () => Effect.void,
786 loadServerConfig: () => Effect.succeed(Option.some(CONFIG)),786 loadServerConfig: () => Effect.succeedSome(CONFIG),
787 saveServerConfig: (_environmentId, config) => Queue.offer(savedConfigs, config),787 saveServerConfig: (_environmentId, config) => Queue.offer(savedConfigs, config),
788 loadVcsRefs: () => Effect.succeed(Option.none()),788 loadVcsRefs: () => Effect.succeedNone,
789 saveVcsRefs: () => Effect.void,789 saveVcsRefs: () => Effect.void,
790 removeVcsRefs: () => Effect.void,790 removeVcsRefs: () => Effect.void,
791 clearVcsRefs: () => Effect.void,791 clearVcsRefs: () => Effect.void,
...@@ -839,14 +839,14 @@ describe("server state projection", () => {...@@ -839,14 +839,14 @@ describe("server state projection", () => {
839 } satisfies EnvironmentSupervisor.EnvironmentSupervisor["Service"]);839 } satisfies EnvironmentSupervisor.EnvironmentSupervisor["Service"]);
840 const savedConfigs = yield* Queue.unbounded<ServerConfig>();840 const savedConfigs = yield* Queue.unbounded<ServerConfig>();
841 const cache = Persistence.EnvironmentCacheStore.of({841 const cache = Persistence.EnvironmentCacheStore.of({
842 loadShell: () => Effect.succeed(Option.none()),842 loadShell: () => Effect.succeedNone,
843 saveShell: () => Effect.void,843 saveShell: () => Effect.void,
844 loadThread: () => Effect.succeed(Option.none()),844 loadThread: () => Effect.succeedNone,
845 saveThread: () => Effect.void,845 saveThread: () => Effect.void,
846 removeThread: () => Effect.void,846 removeThread: () => Effect.void,
847 loadServerConfig: () => Effect.succeed(Option.some(CONFIG)),847 loadServerConfig: () => Effect.succeedSome(CONFIG),
848 saveServerConfig: (_environmentId, config) => Queue.offer(savedConfigs, config),848 saveServerConfig: (_environmentId, config) => Queue.offer(savedConfigs, config),
849 loadVcsRefs: () => Effect.succeed(Option.none()),849 loadVcsRefs: () => Effect.succeedNone,
850 saveVcsRefs: () => Effect.void,850 saveVcsRefs: () => Effect.void,
851 removeVcsRefs: () => Effect.void,851 removeVcsRefs: () => Effect.void,
852 clearVcsRefs: () => Effect.void,852 clearVcsRefs: () => Effect.void,
packages/client-runtime/src/state/serverUsage.test.ts+4-4
...@@ -125,14 +125,14 @@ const makeHarness = Effect.fn("ServerUsageTest.makeHarness")(function* (...@@ -125,14 +125,14 @@ const makeHarness = Effect.fn("ServerUsageTest.makeHarness")(function* (
125 Stream.provideService(stream, EnvironmentSupervisor, supervisor),125 Stream.provideService(stream, EnvironmentSupervisor, supervisor),
126 } as EnvironmentRegistry["Service"]);126 } as EnvironmentRegistry["Service"]);
127 const cache = EnvironmentCacheStore.of({127 const cache = EnvironmentCacheStore.of({
128 loadShell: () => Effect.succeed(Option.none()),128 loadShell: () => Effect.succeedNone,
129 saveShell: () => Effect.void,129 saveShell: () => Effect.void,
130 loadThread: () => Effect.succeed(Option.none()),130 loadThread: () => Effect.succeedNone,
131 saveThread: () => Effect.void,131 saveThread: () => Effect.void,
132 removeThread: () => Effect.void,132 removeThread: () => Effect.void,
133 loadServerConfig: () => Effect.succeed(Option.none()),133 loadServerConfig: () => Effect.succeedNone,
134 saveServerConfig: () => Effect.void,134 saveServerConfig: () => Effect.void,
135 loadVcsRefs: () => Effect.succeed(Option.none()),135 loadVcsRefs: () => Effect.succeedNone,
136 saveVcsRefs: () => Effect.void,136 saveVcsRefs: () => Effect.void,
137 removeVcsRefs: () => Effect.void,137 removeVcsRefs: () => Effect.void,
138 clearVcsRefs: () => Effect.void,138 clearVcsRefs: () => Effect.void,
packages/client-runtime/src/state/session.ts+1-1
...@@ -20,7 +20,7 @@ function initialConfigOption<E>(...@@ -20,7 +20,7 @@ function initialConfigOption<E>(
20 initialConfig: Effect.Effect<ServerConfig, E>,20 initialConfig: Effect.Effect<ServerConfig, E>,
21): Effect.Effect<Option.Option<ServerConfig>> {21): Effect.Effect<Option.Option<ServerConfig>> {
22 return initialConfig.pipe(22 return initialConfig.pipe(
23 Effect.map(Option.some),23 Effect.asSome,
24 Effect.catch((error) =>24 Effect.catch((error) =>
25 Effect.logWarning("Could not load the initial environment configuration.").pipe(25 Effect.logWarning("Could not load the initial environment configuration.").pipe(
26 Effect.annotateLogs({ ...safeErrorLogAttributes(error) }),26 Effect.annotateLogs({ ...safeErrorLogAttributes(error) }),
packages/client-runtime/src/state/shell-sync.test.ts+18-18
...@@ -80,14 +80,14 @@ describe("environment shell synchronization", () => {...@@ -80,14 +80,14 @@ describe("environment shell synchronization", () => {
80 retryNow: Effect.void,80 retryNow: Effect.void,
81 } satisfies EnvironmentSupervisor.EnvironmentSupervisor["Service"]);81 } satisfies EnvironmentSupervisor.EnvironmentSupervisor["Service"]);
82 const cache = Persistence.EnvironmentCacheStore.of({82 const cache = Persistence.EnvironmentCacheStore.of({
83 loadShell: () => Effect.succeed(Option.none()),83 loadShell: () => Effect.succeedNone,
84 saveShell: () => Effect.never,84 saveShell: () => Effect.never,
85 loadThread: () => Effect.succeed(Option.none()),85 loadThread: () => Effect.succeedNone,
86 saveThread: () => Effect.void,86 saveThread: () => Effect.void,
87 removeThread: () => Effect.void,87 removeThread: () => Effect.void,
88 loadServerConfig: () => Effect.succeed(Option.none()),88 loadServerConfig: () => Effect.succeedNone,
89 saveServerConfig: () => Effect.void,89 saveServerConfig: () => Effect.void,
90 loadVcsRefs: () => Effect.succeed(Option.none()),90 loadVcsRefs: () => Effect.succeedNone,
91 saveVcsRefs: () => Effect.void,91 saveVcsRefs: () => Effect.void,
92 removeVcsRefs: () => Effect.void,92 removeVcsRefs: () => Effect.void,
93 clearVcsRefs: () => Effect.void,93 clearVcsRefs: () => Effect.void,
...@@ -96,7 +96,7 @@ describe("environment shell synchronization", () => {...@@ -96,7 +96,7 @@ describe("environment shell synchronization", () => {
96 // Cold cache with no HTTP snapshot available → falls back to the96 // Cold cache with no HTTP snapshot available → falls back to the
97 // socket-embedded snapshot.97 // socket-embedded snapshot.
98 const snapshotLoader = ShellSnapshotLoader.of({98 const snapshotLoader = ShellSnapshotLoader.of({
99 load: () => Effect.succeed(Option.none()),99 load: () => Effect.succeedNone,
100 });100 });
101 const shellState = yield* makeEnvironmentShellState().pipe(101 const shellState = yield* makeEnvironmentShellState().pipe(
102 Effect.provideService(EnvironmentSupervisor.EnvironmentSupervisor, supervisor),102 Effect.provideService(EnvironmentSupervisor.EnvironmentSupervisor, supervisor),
...@@ -176,14 +176,14 @@ describe("environment shell synchronization", () => {...@@ -176,14 +176,14 @@ describe("environment shell synchronization", () => {
176 retryNow: Effect.void,176 retryNow: Effect.void,
177 } satisfies EnvironmentSupervisor.EnvironmentSupervisor["Service"]);177 } satisfies EnvironmentSupervisor.EnvironmentSupervisor["Service"]);
178 const cache = Persistence.EnvironmentCacheStore.of({178 const cache = Persistence.EnvironmentCacheStore.of({
179 loadShell: () => Effect.succeed(Option.none()),179 loadShell: () => Effect.succeedNone,
180 saveShell: () => Effect.void,180 saveShell: () => Effect.void,
181 loadThread: () => Effect.succeed(Option.none()),181 loadThread: () => Effect.succeedNone,
182 saveThread: () => Effect.void,182 saveThread: () => Effect.void,
183 removeThread: () => Effect.void,183 removeThread: () => Effect.void,
184 loadServerConfig: () => Effect.succeed(Option.none()),184 loadServerConfig: () => Effect.succeedNone,
185 saveServerConfig: () => Effect.void,185 saveServerConfig: () => Effect.void,
186 loadVcsRefs: () => Effect.succeed(Option.none()),186 loadVcsRefs: () => Effect.succeedNone,
187 saveVcsRefs: () => Effect.void,187 saveVcsRefs: () => Effect.void,
188 removeVcsRefs: () => Effect.void,188 removeVcsRefs: () => Effect.void,
189 clearVcsRefs: () => Effect.void,189 clearVcsRefs: () => Effect.void,
...@@ -194,7 +194,7 @@ describe("environment shell synchronization", () => {...@@ -194,7 +194,7 @@ describe("environment shell synchronization", () => {
194 Effect.provideService(Persistence.EnvironmentCacheStore, cache),194 Effect.provideService(Persistence.EnvironmentCacheStore, cache),
195 Effect.provideService(195 Effect.provideService(
196 ShellSnapshotLoader,196 ShellSnapshotLoader,
197 ShellSnapshotLoader.of({ load: () => Effect.succeed(Option.none()) }),197 ShellSnapshotLoader.of({ load: () => Effect.succeedNone }),
198 ),198 ),
199 );199 );
200 yield* SubscriptionRef.set(supervisorState, {200 yield* SubscriptionRef.set(supervisorState, {
...@@ -284,14 +284,14 @@ describe("environment shell synchronization", () => {...@@ -284,14 +284,14 @@ describe("environment shell synchronization", () => {
284 retryNow: Effect.void,284 retryNow: Effect.void,
285 } satisfies EnvironmentSupervisor.EnvironmentSupervisor["Service"]);285 } satisfies EnvironmentSupervisor.EnvironmentSupervisor["Service"]);
286 const cache = Persistence.EnvironmentCacheStore.of({286 const cache = Persistence.EnvironmentCacheStore.of({
287 loadShell: () => Effect.succeed(Option.some(cachedSnapshot)),287 loadShell: () => Effect.succeedSome(cachedSnapshot),
288 saveShell: () => Effect.void,288 saveShell: () => Effect.void,
289 loadThread: () => Effect.succeed(Option.none()),289 loadThread: () => Effect.succeedNone,
290 saveThread: () => Effect.void,290 saveThread: () => Effect.void,
291 removeThread: () => Effect.void,291 removeThread: () => Effect.void,
292 loadServerConfig: () => Effect.succeed(Option.none()),292 loadServerConfig: () => Effect.succeedNone,
293 saveServerConfig: () => Effect.void,293 saveServerConfig: () => Effect.void,
294 loadVcsRefs: () => Effect.succeed(Option.none()),294 loadVcsRefs: () => Effect.succeedNone,
295 saveVcsRefs: () => Effect.void,295 saveVcsRefs: () => Effect.void,
296 removeVcsRefs: () => Effect.void,296 removeVcsRefs: () => Effect.void,
297 clearVcsRefs: () => Effect.void,297 clearVcsRefs: () => Effect.void,
...@@ -364,14 +364,14 @@ describe("environment shell synchronization", () => {...@@ -364,14 +364,14 @@ describe("environment shell synchronization", () => {
364 retryNow: Effect.void,364 retryNow: Effect.void,
365 } satisfies EnvironmentSupervisor.EnvironmentSupervisor["Service"]);365 } satisfies EnvironmentSupervisor.EnvironmentSupervisor["Service"]);
366 const cache = Persistence.EnvironmentCacheStore.of({366 const cache = Persistence.EnvironmentCacheStore.of({
367 loadShell: () => Effect.succeed(Option.some(LIVE_SHELL_SNAPSHOT)),367 loadShell: () => Effect.succeedSome(LIVE_SHELL_SNAPSHOT),
368 saveShell: () => Effect.void,368 saveShell: () => Effect.void,
369 loadThread: () => Effect.succeed(Option.none()),369 loadThread: () => Effect.succeedNone,
370 saveThread: () => Effect.void,370 saveThread: () => Effect.void,
371 removeThread: () => Effect.void,371 removeThread: () => Effect.void,
372 loadServerConfig: () => Effect.succeed(Option.none()),372 loadServerConfig: () => Effect.succeedNone,
373 saveServerConfig: () => Effect.void,373 saveServerConfig: () => Effect.void,
374 loadVcsRefs: () => Effect.succeed(Option.none()),374 loadVcsRefs: () => Effect.succeedNone,
375 saveVcsRefs: () => Effect.void,375 saveVcsRefs: () => Effect.void,
376 removeVcsRefs: () => Effect.void,376 removeVcsRefs: () => Effect.void,
377 clearVcsRefs: () => Effect.void,377 clearVcsRefs: () => Effect.void,
packages/client-runtime/src/state/sourceControl.test.ts+4-4
...@@ -101,14 +101,14 @@ describe("source control environment atoms", () => {...@@ -101,14 +101,14 @@ describe("source control environment atoms", () => {
101 } as unknown as EnvironmentRegistry.EnvironmentRegistry["Service"]);101 } as unknown as EnvironmentRegistry.EnvironmentRegistry["Service"]);
102 const removed = new Array<string>();102 const removed = new Array<string>();
103 const cache = Persistence.EnvironmentCacheStore.of({103 const cache = Persistence.EnvironmentCacheStore.of({
104 loadShell: () => Effect.succeed(Option.none()),104 loadShell: () => Effect.succeedNone,
105 saveShell: () => Effect.void,105 saveShell: () => Effect.void,
106 loadThread: () => Effect.succeed(Option.none()),106 loadThread: () => Effect.succeedNone,
107 saveThread: () => Effect.void,107 saveThread: () => Effect.void,
108 removeThread: () => Effect.void,108 removeThread: () => Effect.void,
109 loadServerConfig: () => Effect.succeed(Option.none()),109 loadServerConfig: () => Effect.succeedNone,
110 saveServerConfig: () => Effect.void,110 saveServerConfig: () => Effect.void,
111 loadVcsRefs: () => Effect.succeed(Option.none()),111 loadVcsRefs: () => Effect.succeedNone,
112 saveVcsRefs: () => Effect.void,112 saveVcsRefs: () => Effect.void,
113 removeVcsRefs: (environmentId, cwd) =>113 removeVcsRefs: (environmentId, cwd) =>
114 Effect.sync(() => {114 Effect.sync(() => {
packages/client-runtime/src/state/threads-atoms.test.ts+3-3
...@@ -200,7 +200,7 @@ const makeHarness = Effect.fn("TestThreadAtoms.makeHarness")(function* (options?...@@ -200,7 +200,7 @@ const makeHarness = Effect.fn("TestThreadAtoms.makeHarness")(function* (options?
200 Layer.succeed(200 Layer.succeed(
201 EnvironmentCacheStore,201 EnvironmentCacheStore,
202 EnvironmentCacheStore.of({202 EnvironmentCacheStore.of({
203 loadShell: () => Effect.succeed(Option.none()),203 loadShell: () => Effect.succeedNone,
204 saveShell: () => Effect.void,204 saveShell: () => Effect.void,
205 loadThread: () =>205 loadThread: () =>
206 Effect.sync(() => {206 Effect.sync(() => {
...@@ -209,9 +209,9 @@ const makeHarness = Effect.fn("TestThreadAtoms.makeHarness")(function* (options?...@@ -209,9 +209,9 @@ const makeHarness = Effect.fn("TestThreadAtoms.makeHarness")(function* (options?
209 }),209 }),
210 saveThread: () => Effect.void,210 saveThread: () => Effect.void,
211 removeThread: () => Effect.void,211 removeThread: () => Effect.void,
212 loadServerConfig: () => Effect.succeed(Option.none()),212 loadServerConfig: () => Effect.succeedNone,
213 saveServerConfig: () => Effect.void,213 saveServerConfig: () => Effect.void,
214 loadVcsRefs: () => Effect.succeed(Option.none()),214 loadVcsRefs: () => Effect.succeedNone,
215 saveVcsRefs: () => Effect.void,215 saveVcsRefs: () => Effect.void,
216 removeVcsRefs: () => Effect.void,216 removeVcsRefs: () => Effect.void,
217 clearVcsRefs: () => Effect.void,217 clearVcsRefs: () => Effect.void,
packages/client-runtime/src/state/threads-pagination.test.ts+3-3
...@@ -197,16 +197,16 @@ const makeHarness = Effect.fn("TestThreadPagination.makeHarness")(function* (opt...@@ -197,16 +197,16 @@ const makeHarness = Effect.fn("TestThreadPagination.makeHarness")(function* (opt
197 retryNow: Effect.void,197 retryNow: Effect.void,
198 } satisfies EnvironmentSupervisor.EnvironmentSupervisor["Service"]);198 } satisfies EnvironmentSupervisor.EnvironmentSupervisor["Service"]);
199 const cache = Persistence.EnvironmentCacheStore.of({199 const cache = Persistence.EnvironmentCacheStore.of({
200 loadShell: () => Effect.succeed(Option.none()),200 loadShell: () => Effect.succeedNone,
201 saveShell: () => Effect.void,201 saveShell: () => Effect.void,
202 loadThread: () =>202 loadThread: () =>
203 Effect.succeed(options?.cached !== undefined ? Option.some(options.cached) : Option.none()),203 Effect.succeed(options?.cached !== undefined ? Option.some(options.cached) : Option.none()),
204 saveThread: (_environmentId, thread) =>204 saveThread: (_environmentId, thread) =>
205 Ref.update(savedThreads, (current) => [...current, thread]),205 Ref.update(savedThreads, (current) => [...current, thread]),
206 removeThread: () => Effect.void,206 removeThread: () => Effect.void,
207 loadServerConfig: () => Effect.succeed(Option.none()),207 loadServerConfig: () => Effect.succeedNone,
208 saveServerConfig: () => Effect.void,208 saveServerConfig: () => Effect.void,
209 loadVcsRefs: () => Effect.succeed(Option.none()),209 loadVcsRefs: () => Effect.succeedNone,
210 saveVcsRefs: () => Effect.void,210 saveVcsRefs: () => Effect.void,
211 removeVcsRefs: () => Effect.void,211 removeVcsRefs: () => Effect.void,
212 clearVcsRefs: () => Effect.void,212 clearVcsRefs: () => Effect.void,
packages/client-runtime/src/state/threads-sync.test.ts+3-3
...@@ -216,7 +216,7 @@ const makeHarness = Effect.fn("TestEnvironmentThreads.makeHarness")(function* (o...@@ -216,7 +216,7 @@ const makeHarness = Effect.fn("TestEnvironmentThreads.makeHarness")(function* (o
216 retryNow: Ref.update(retryCount, (count) => count + 1),216 retryNow: Ref.update(retryCount, (count) => count + 1),
217 } satisfies EnvironmentSupervisor.EnvironmentSupervisor["Service"]);217 } satisfies EnvironmentSupervisor.EnvironmentSupervisor["Service"]);
218 const cache = Persistence.EnvironmentCacheStore.of({218 const cache = Persistence.EnvironmentCacheStore.of({
219 loadShell: () => Effect.succeed(Option.none()),219 loadShell: () => Effect.succeedNone,
220 saveShell: () => Effect.void,220 saveShell: () => Effect.void,
221 loadThread: (_environmentId, threadId) =>221 loadThread: (_environmentId, threadId) =>
222 options?.loadCached ??222 options?.loadCached ??
...@@ -234,9 +234,9 @@ const makeHarness = Effect.fn("TestEnvironmentThreads.makeHarness")(function* (o...@@ -234,9 +234,9 @@ const makeHarness = Effect.fn("TestEnvironmentThreads.makeHarness")(function* (o
234 ),234 ),
235 removeThread: (_environmentId, threadId) =>235 removeThread: (_environmentId, threadId) =>
236 Ref.update(removedThreads, (current) => [...current, threadId]),236 Ref.update(removedThreads, (current) => [...current, threadId]),
237 loadServerConfig: () => Effect.succeed(Option.none()),237 loadServerConfig: () => Effect.succeedNone,
238 saveServerConfig: () => Effect.void,238 saveServerConfig: () => Effect.void,
239 loadVcsRefs: () => Effect.succeed(Option.none()),239 loadVcsRefs: () => Effect.succeedNone,
240 saveVcsRefs: () => Effect.void,240 saveVcsRefs: () => Effect.void,
241 removeVcsRefs: () => Effect.void,241 removeVcsRefs: () => Effect.void,
242 clearVcsRefs: () => Effect.void,242 clearVcsRefs: () => Effect.void,
packages/client-runtime/src/state/vcs.test.ts+3-3
...@@ -98,12 +98,12 @@ function cacheWithRefs(...@@ -98,12 +98,12 @@ function cacheWithRefs(
98 overrides: Partial<Persistence.EnvironmentCacheStore["Service"]> = {},98 overrides: Partial<Persistence.EnvironmentCacheStore["Service"]> = {},
99) {99) {
100 return Persistence.EnvironmentCacheStore.of({100 return Persistence.EnvironmentCacheStore.of({
101 loadShell: () => Effect.succeed(Option.none()),101 loadShell: () => Effect.succeedNone,
102 saveShell: () => Effect.void,102 saveShell: () => Effect.void,
103 loadThread: () => Effect.succeed(Option.none()),103 loadThread: () => Effect.succeedNone,
104 saveThread: () => Effect.void,104 saveThread: () => Effect.void,
105 removeThread: () => Effect.void,105 removeThread: () => Effect.void,
106 loadServerConfig: () => Effect.succeed(Option.none()),106 loadServerConfig: () => Effect.succeedNone,
107 saveServerConfig: () => Effect.void,107 saveServerConfig: () => Effect.void,
108 loadVcsRefs: () => Effect.succeed(refs),108 loadVcsRefs: () => Effect.succeed(refs),
109 saveVcsRefs: () => Effect.void,109 saveVcsRefs: () => Effect.void,
packages/client-runtime/src/state/vcsAction.test.ts+4-4
...@@ -99,14 +99,14 @@ function progress<T extends GitActionProgressEvent>(event: T): T {...@@ -99,14 +99,14 @@ function progress<T extends GitActionProgressEvent>(event: T): T {
9999
100function cacheStore(onClearVcsRefs: (environmentId: EnvironmentId) => void) {100function cacheStore(onClearVcsRefs: (environmentId: EnvironmentId) => void) {
101 return Persistence.EnvironmentCacheStore.of({101 return Persistence.EnvironmentCacheStore.of({
102 loadShell: () => Effect.succeed(Option.none()),102 loadShell: () => Effect.succeedNone,
103 saveShell: () => Effect.void,103 saveShell: () => Effect.void,
104 loadThread: () => Effect.succeed(Option.none()),104 loadThread: () => Effect.succeedNone,
105 saveThread: () => Effect.void,105 saveThread: () => Effect.void,
106 removeThread: () => Effect.void,106 removeThread: () => Effect.void,
107 loadServerConfig: () => Effect.succeed(Option.none()),107 loadServerConfig: () => Effect.succeedNone,
108 saveServerConfig: () => Effect.void,108 saveServerConfig: () => Effect.void,
109 loadVcsRefs: () => Effect.succeed(Option.none()),109 loadVcsRefs: () => Effect.succeedNone,
110 saveVcsRefs: () => Effect.void,110 saveVcsRefs: () => Effect.void,
111 removeVcsRefs: () => Effect.void,111 removeVcsRefs: () => Effect.void,
112 clearVcsRefs: (environmentId) => Effect.sync(() => onClearVcsRefs(environmentId)),112 clearVcsRefs: (environmentId) => Effect.sync(() => onClearVcsRefs(environmentId)),
packages/contracts/src/pullRequest.test.ts+1-1
...@@ -92,7 +92,7 @@ describe("PullRequestListResult", () => {...@@ -92,7 +92,7 @@ describe("PullRequestListResult", () => {
92 it("round-trips through the JSON codec the RPC serializes with", () => {92 it("round-trips through the JSON codec the RPC serializes with", () => {
93 const codec = Schema.toCodecJson(PullRequestListResult);93 const codec = Schema.toCodecJson(PullRequestListResult);
9494
95 const decoded = Schema.decodeUnknownSync(codec)(Schema.encodeUnknownSync(codec)(LIST_RESULT));95 const decoded = Schema.decodeSync(codec)(Schema.encodeUnknownSync(codec)(LIST_RESULT));
9696
97 expect(decoded).toStrictEqual(LIST_RESULT);97 expect(decoded).toStrictEqual(LIST_RESULT);
98 });98 });
packages/contracts/src/rpc.test.ts+3-3
...@@ -13,19 +13,19 @@ import { WsSubscribeServerConfigRpc } from "./rpc.ts";...@@ -13,19 +13,19 @@ import { WsSubscribeServerConfigRpc } from "./rpc.ts";
13describe("subscribeServerConfig payload compatibility", () => {13describe("subscribeServerConfig payload compatibility", () => {
14 it("is accepted by a server whose schema predates the field", () => {14 it("is accepted by a server whose schema predates the field", () => {
15 const oldServerPayload = Schema.Struct({});15 const oldServerPayload = Schema.Struct({});
16 const decoded = Schema.decodeUnknownExit(oldServerPayload)({ environmentThemes: true });16 const decoded = Schema.decodeExit(oldServerPayload)({ environmentThemes: true });
17 expect(Exit.isSuccess(decoded)).toBe(true);17 expect(Exit.isSuccess(decoded)).toBe(true);
18 });18 });
1919
20 it("is carried by a server that declares it", () => {20 it("is carried by a server that declares it", () => {
21 const decoded = Schema.decodeUnknownSync(WsSubscribeServerConfigRpc.payloadSchema)({21 const decoded = Schema.decodeSync(WsSubscribeServerConfigRpc.payloadSchema)({
22 environmentThemes: true,22 environmentThemes: true,
23 });23 });
24 expect(decoded).toEqual({ environmentThemes: true });24 expect(decoded).toEqual({ environmentThemes: true });
25 });25 });
2626
27 it("stays optional, so a client that never sends it still subscribes", () => {27 it("stays optional, so a client that never sends it still subscribes", () => {
28 const decoded = Schema.decodeUnknownSync(WsSubscribeServerConfigRpc.payloadSchema)({});28 const decoded = Schema.decodeSync(WsSubscribeServerConfigRpc.payloadSchema)({});
29 expect(decoded).toEqual({});29 expect(decoded).toEqual({});
30 });30 });
31});31});
packages/contracts/src/settings.ts+1-3
...@@ -1082,9 +1082,7 @@ export type StorageCleanupSettings = typeof StorageCleanupSettings.Type;...@@ -1082,9 +1082,7 @@ export type StorageCleanupSettings = typeof StorageCleanupSettings.Type;
1082export const ServerSettings = Schema.Struct({1082export const ServerSettings = Schema.Struct({
1083 worktreeCleanup: WorktreeCleanup.pipe(Schema.withDecodingDefault(Effect.succeed(null))),1083 worktreeCleanup: WorktreeCleanup.pipe(Schema.withDecodingDefault(Effect.succeed(null))),
1084 storageCleanup: StorageCleanupSettings.pipe(1084 storageCleanup: StorageCleanupSettings.pipe(
1085 Schema.withDecodingDefault(1085 Schema.withDecodingDefault(Effect.succeed(Schema.decodeSync(StorageCleanupSettings)({}))),
1086 Effect.succeed(Schema.decodeUnknownSync(StorageCleanupSettings)({})),
1087 ),
1088 ),1086 ),
1089 // How assistant text reaches clients during a turn. Deliberately a fresh1087 // How assistant text reaches clients during a turn. Deliberately a fresh
1090 // key (was `enableLegacyTokenStreaming`, before that1088 // key (was `enableLegacyTokenStreaming`, before that
packages/effect-acp/src/client.ts+3-5
...@@ -346,11 +346,9 @@ export const make = Effect.fn("effect-acp/AcpClient.make")(function* (...@@ -346,11 +346,9 @@ export const make = Effect.fn("effect-acp/AcpClient.make")(function* (
346 registration: BufferedNotificationHandler<A>,346 registration: BufferedNotificationHandler<A>,
347 notification: A,347 notification: A,
348 ) =>348 ) =>
349 Effect.forEach(349 Effect.forEach(registration.handlers, (handler) => handler(notification).pipe(Effect.ignore), {
350 registration.handlers,350 discard: true,
351 (handler) => handler(notification).pipe(Effect.catch(() => Effect.void)),351 });
352 { discard: true },
353 );
354352
355 const flushBufferedNotifications = <A>(registration: BufferedNotificationHandler<A>) =>353 const flushBufferedNotifications = <A>(registration: BufferedNotificationHandler<A>) =>
356 Effect.suspend(() => {354 Effect.suspend(() => {
packages/effect-acp/src/protocol.ts+1-1
...@@ -216,7 +216,7 @@ export const makeAcpPatchedProtocol = Effect.fn("makeAcpPatchedProtocol")(functi...@@ -216,7 +216,7 @@ export const makeAcpPatchedProtocol = Effect.fn("makeAcpPatchedProtocol")(functi
216 Queue.offer(notificationQueue, notification).pipe(216 Queue.offer(notificationQueue, notification).pipe(
217 Effect.andThen(217 Effect.andThen(
218 options.onNotification218 options.onNotification
219 ? options.onNotification(notification).pipe(Effect.catch(() => Effect.void))219 ? options.onNotification(notification).pipe(Effect.ignore)
220 : Effect.void,220 : Effect.void,
221 ),221 ),
222 Effect.asVoid,222 Effect.asVoid,
packages/effect-codex-app-server/src/_internal/shared.ts+1-1
...@@ -45,7 +45,7 @@ export const encodeOptionalPayload = <A, I>(...@@ -45,7 +45,7 @@ export const encodeOptionalPayload = <A, I>(
45): Effect.Effect<I | undefined, CodexError.CodexAppServerRequestError> => {45): Effect.Effect<I | undefined, CodexError.CodexAppServerRequestError> => {
46 if (!schema) {46 if (!schema) {
47 if (payload === undefined) {47 if (payload === undefined) {
48 return Effect.sync(() => undefined);48 return Effect.undefined;
49 }49 }
50 return Effect.fail(50 return Effect.fail(
51 CodexError.CodexAppServerRequestError.unexpectedPayload(method, "encode-payload", payload),51 CodexError.CodexAppServerRequestError.unexpectedPayload(method, "encode-payload", payload),
packages/effect-codex-app-server/src/client.ts+2-4
...@@ -153,14 +153,12 @@ const make = Effect.fn("effect-codex-app-server/CodexAppServerClient.make")(func...@@ -153,14 +153,12 @@ const make = Effect.fn("effect-codex-app-server/CodexAppServerClient.make")(func
153 Effect.flatMap((decoded) =>153 Effect.flatMap((decoded) =>
154 Effect.forEach(handlers, (handler) => handler(decoded), { discard: true }),154 Effect.forEach(handlers, (handler) => handler(decoded), { discard: true }),
155 ),155 ),
156 Effect.catch(() => Effect.void),156 Effect.ignore,
157 );157 );
158 }158 }
159159
160 return unknownNotificationHandler160 return unknownNotificationHandler
161 ? unknownNotificationHandler(notification.method, notification.params).pipe(161 ? unknownNotificationHandler(notification.method, notification.params).pipe(Effect.ignore)
162 Effect.catch(() => Effect.void),
163 )
164 : Effect.void;162 : Effect.void;
165 };163 };
166164
packages/effect-codex-app-server/src/schema.test.ts+1-1
...@@ -29,7 +29,7 @@ it("keeps async questions in live notifications and thread history", () => {...@@ -29,7 +29,7 @@ it("keeps async questions in live notifications and thread history", () => {
29 CodexSchema.V2ThreadReadResponse__ThreadItem,29 CodexSchema.V2ThreadReadResponse__ThreadItem,
30 CodexSchema.V2ThreadResumeResponse__ThreadItem,30 CodexSchema.V2ThreadResumeResponse__ThreadItem,
31 ]) {31 ]) {
32 assert.deepEqual(Schema.decodeUnknownSync(schema)(item), item);32 assert.deepEqual(Schema.decodeSync(schema)(item), item);
33 }33 }
34});34});
3535
packages/shared/src/httpReadiness.ts+7-11
...@@ -106,17 +106,13 @@ export const waitForHttpReady = Effect.fn("shared.httpReadiness.waitForHttpReady...@@ -106,17 +106,13 @@ export const waitForHttpReady = Effect.fn("shared.httpReadiness.waitForHttpReady
106 Effect.timeoutOption(Duration.millis(probeTimeoutMs)),106 Effect.timeoutOption(Duration.millis(probeTimeoutMs)),
107 Effect.mapError((cause) => fail(cause)),107 Effect.mapError((cause) => fail(cause)),
108 );108 );
109 return yield* Option.match(responseOption, {109 return yield* Effect.fromOption(responseOption, () =>
110 onSome: Effect.succeed,110 fail({
111 onNone: () =>111 kind: "probe-timeout",
112 Effect.fail(112 attempt,
113 fail({113 probeTimeoutMs,
114 kind: "probe-timeout",114 }),
115 attempt,115 );
116 probeTimeoutMs,
117 }),
118 ),
119 });
120 }).pipe(116 }).pipe(
121 Effect.mapError((cause) => (isMadeError(cause) ? cause : fail(cause))),117 Effect.mapError((cause) => (isMadeError(cause) ? cause : fail(cause))),
122 Effect.tapError((cause) =>118 Effect.tapError((cause) =>
packages/shared/src/relayClient.ts+11-13
...@@ -331,9 +331,7 @@ export const makeCloudflaredRelayClient = Effect.fn("cloudflared.make")(function...@@ -331,9 +331,7 @@ export const makeCloudflaredRelayClient = Effect.fn("cloudflared.make")(function
331 for (let attempt = 0; attempt < INSTALL_LOCK_RETRY_COUNT; attempt += 1) {331 for (let attempt = 0; attempt < INSTALL_LOCK_RETRY_COUNT; attempt += 1) {
332 const acquired = yield* fileSystem.writeFileString(lockPath, "", { flag: "wx" }).pipe(332 const acquired = yield* fileSystem.writeFileString(lockPath, "", { flag: "wx" }).pipe(
333 Effect.as(true),333 Effect.as(true),
334 Effect.catch((error) =>334 Effect.catchIf(isAlreadyExists, () => Effect.succeed(false)),
335 isAlreadyExists(error) ? Effect.succeed(false) : Effect.fail(error),
336 ),
337 );335 );
338 if (acquired) return;336 if (acquired) return;
339337
...@@ -445,16 +443,16 @@ export const makeCloudflaredRelayClient = Effect.fn("cloudflared.make")(function...@@ -445,16 +443,16 @@ export const makeCloudflaredRelayClient = Effect.fn("cloudflared.make")(function
445 }).pipe(443 }).pipe(
446 Effect.scoped,444 Effect.scoped,
447 Effect.ensuring(fileSystem.remove(lockPath, { force: true }).pipe(Effect.ignore)),445 Effect.ensuring(fileSystem.remove(lockPath, { force: true }).pipe(Effect.ignore)),
448 Effect.catch((cause) =>446 Effect.catchIf(
449 cause instanceof RelayClientInstallError447 (cause) => !(cause instanceof RelayClientInstallError),
450 ? Effect.fail(cause)448 (cause) =>
451 : Effect.fail(449 Effect.fail(
452 new RelayClientInstallError({450 new RelayClientInstallError({
453 reason: "write_failed",451 reason: "write_failed",
454 message: "Could not install the relay client.",452 message: "Could not install the relay client.",
455 cause,453 cause,
456 }),454 }),
457 ),455 ),
458 ),456 ),
459 );457 );
460 });458 });
packages/shared/src/relayTracing.ts+1-1
...@@ -151,6 +151,6 @@ export function makeRelayClientTracingLayer(...@@ -151,6 +151,6 @@ export function makeRelayClientTracingLayer(
151151
152 return Layer.effect(152 return Layer.effect(
153 RelayClientTracer,153 RelayClientTracer,
154 Tracer.Tracer.pipe(Effect.map(nonInterferingTracer), Effect.map(Option.some)),154 Tracer.Tracer.pipe(Effect.map(nonInterferingTracer), Effect.asSome),
155 ).pipe(Layer.provide(tracerLayer));155 ).pipe(Layer.provide(tracerLayer));
156}156}
packages/shared/src/schemaJson.ts+1-2
...@@ -1,7 +1,6 @@...@@ -1,7 +1,6 @@
1import * as Cause from "effect/Cause";1import * as Cause from "effect/Cause";
2import * as Effect from "effect/Effect";2import * as Effect from "effect/Effect";
3import * as Exit from "effect/Exit";3import * as Exit from "effect/Exit";
4import * as Option from "effect/Option";
5import * as Result from "effect/Result";4import * as Result from "effect/Result";
6import * as Schema from "effect/Schema";5import * as Schema from "effect/Schema";
7import * as SchemaGetter from "effect/SchemaGetter";6import * as SchemaGetter from "effect/SchemaGetter";
...@@ -187,7 +186,7 @@ const parseLenientJsonGetter = SchemaGetter.onSome((input: string) => {...@@ -187,7 +186,7 @@ const parseLenientJsonGetter = SchemaGetter.onSome((input: string) => {
187 );186 );
188187
189 return decodeJsonString(stripped).pipe(188 return decodeJsonString(stripped).pipe(
190 Effect.map(Option.some),189 Effect.asSome,
191 Effect.mapError((error) => error.issue),190 Effect.mapError((error) => error.issue),
192 );191 );
193});192});
tsconfig.base.json+2-1
...@@ -50,7 +50,8 @@...@@ -50,7 +50,8 @@
50 "globalTimers": "error",50 "globalTimers": "error",
51 "globalTimersInEffect": "error",51 "globalTimersInEffect": "error",
52 "globalFetch": "error",52 "globalFetch": "error",
53 "globalFetchInEffect": "error"53 "globalFetchInEffect": "error",
54 "schemaNumber": "off"
54 }55 }
55 }56 }
56 ]57 ]