Skip to content

Commit fd4b099

Browse files
committed
chore: integrate main Effect tracing
2 parents 2d2ba2b + cfb602a commit fd4b099

231 files changed

Lines changed: 24602 additions & 23684 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.macroscope/check-run-agents/effect-service-conventions.md‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,18 @@ Review changed TypeScript for the conventions below. They apply when a pull requ
4444
- Use plain `make` and `layer` in a module named for its implementation (`BunPtyAdapter.ts`). Keep implementation-specific names when one abstract port module holds several implementations (`makeCloudflaredRelayClient`, `layerCloudflared` in `RelayClient.ts`). `infra/relay/src/db.ts` may keep its inline `Layer.succeed(RelayDb, db)`.
4545
- When a service moves, delete the old files and update every consumer, including orchestration, MCP, tests, and integration harnesses. Do not leave compatibility re-export shims.
4646

47+
## Effect functions and tracing
48+
49+
- Reusable functions use named `Effect.fn("Service.operation")`, not functions wrapping
50+
`Effect.gen`. Inline programs, callbacks, and layer construction can keep `Effect.gen`.
51+
- Named `Effect.fn` creates a span; the unnamed form does not. Default to named spans for service
52+
operations and I/O. `Effect.fnUntraced` is deliberate for per-chunk/per-event work, tight loops,
53+
or helpers duplicating an existing tracing boundary; require a nearby reason for new uses.
54+
- Preserve existing span names/options and avoid adding duplicate spans. Pass combinators after
55+
the generator body; transforms receive the original function arguments after the effect.
56+
- Preserve generic signatures, laziness, resource scope, and interruption behavior. An explicit
57+
generator return type is `Effect.fn.Return<A, E, R>`.
58+
4759
## Dependency acquisition and runtime boundaries
4860

4961
- Production service construction acquires its Effect dependencies from the environment with `yield* Foo.Foo`, and `make`/`layer` types expose those requirements. Flag a factory that takes `Foo["Service"]` (or an object of Effect-returning methods) as a parameter when that value is a service dependency. Passing service instances explicitly in tests is fine; passing pure configuration, immutable domain values, or deliberate callback strategies is not service injection.

‎AGENTS.md‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,12 @@ Architecture and its constraints: `docs/internals/overview.md`. Glossary: `docs/
153153
- Client mutations use `createEnvironmentRpcCommand`. Add protected methods to `CLIENT_GUARDED_RPC_SCOPES` in contracts and use the command's `permissionAtom` for UI availability. Grants are checked at execution against the destination environment; the server remains authoritative. Keep raw RPC clients inside `rpc/`, and extend the permission behavior tests when adding a protected method.
154154
- Server features are services; transports stay thin. A `ws.ts` RPC handler, HTTP route, or MCP tool decodes input, calls one service method, and maps errors. See [Effect services](docs/internals/effect-services.md).
155155
- `apps/web/src/components/ui` exports own their look. Pick a `variant` or `size`; do not restyle one with `className`. If none fits and the look is a generic concept, add a variant to the component; a look that belongs to one feature stays in that feature's own component, not in `components/ui`. Layout classes (width, flex, margin, position) belong on the parent. `shadcn/no-restyle` fails lint on violations. See [Web UI](docs/internals/web-ui.md).
156+
- Reusable Effect functions use `Effect.fn("Service.operation")` with a stable span name, not a
157+
function wrapping `Effect.gen`. Keep `Effect.gen` for inline programs and layer construction.
158+
Use `Effect.fnUntraced` only when spans would be too frequent (stream chunks, per-event
159+
projection, tight loops) or duplicate an existing tracing boundary; explain that choice nearby.
160+
Named `Effect.fn` creates a span; the unnamed form does not. Preserve existing span names and
161+
options, and pass combinators as additional function-builder arguments.
156162
- Inferred types over annotations. `any` is the enemy.
157163
- Comments say why: a constraint, an external quirk, or an invariant the code can't show. Don't restate names or signatures, narrate steps, or record history. `t3code/no-low-value-comments` catches the mechanical cases.
158164
- Our users drive agents all day and notice a dropped frame, a lying spinner, and a stale label. No continuously repainting animations; they peg the GPU on high-refresh displays.

‎apps/desktop/src/app/DesktopCliCommand.ts‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -83,15 +83,17 @@ export const make = Effect.gen(function* () {
8383
Effect.orElseSucceed(() => false),
8484
);
8585

86-
const isOurLink = (link: string) =>
87-
Effect.gen(function* () {
86+
const isOurLink = Effect.fn("DesktopCliCommand.isOurLink")(
87+
function* (link: string) {
8888
yield* fs.readLink(link);
8989
// The launcher is a few KB; never read a large binary another `t3` links to.
9090
const info = yield* fs.stat(link);
9191
if (info.type !== "File" || Number(info.size) > 16_384) return false;
9292
const content = yield* fs.readFileString(link);
9393
return content.includes(DesktopCliShim.MARKER);
94-
}).pipe(Effect.orElseSucceed(() => false));
94+
},
95+
Effect.orElseSucceed(() => false),
96+
);
9597

9698
const powershell = (env: Record<string, string>) =>
9799
Effect.scoped(

‎apps/desktop/src/app/DesktopLegacyLocalStorage.ts‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,8 @@ const make = Effect.gen(function* () {
3434
const markerPath = yield* Ref.make(Option.none<string>());
3535

3636
/** Newest file mtime in a directory; appending to a log leaves the directory's own mtime alone. */
37-
const newestFileMtime = (directory: string) =>
38-
Effect.gen(function* () {
37+
const newestFileMtime = Effect.fn("DesktopLegacyLocalStorage.newestFileMtime")(
38+
function* (directory: string) {
3939
let newest = 0;
4040
for (const name of yield* fs.readDirectory(directory)) {
4141
const info = yield* fs.stat(path.join(directory, name)).pipe(Effect.option);
@@ -47,7 +47,9 @@ const make = Effect.gen(function* () {
4747
newest = Math.max(newest, mtime);
4848
}
4949
return newest;
50-
}).pipe(Effect.option);
50+
},
51+
(effect, _directory) => effect.pipe(Effect.option),
52+
);
5153

5254
const findV1LocalStorage = Effect.gen(function* () {
5355
let newest: { readonly directory: string; readonly mtime: number } | null = null;

‎apps/desktop/src/backend/DesktopBackendConfiguration.ts‎

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -875,20 +875,24 @@ export const make = Effect.gen(function* () {
875875
}
876876
return distro ? `WSL (${distro})` : "WSL";
877877
}).pipe(Effect.withSpan("desktop.backendConfiguration.resolvePrimaryLabel")),
878-
resolveWsl: (input) =>
879-
Effect.gen(function* () {
878+
// The span options depend on each request's port and distro.
879+
resolveWsl: Effect.fnUntraced(
880+
function* (input) {
880881
const shared = yield* sharedInputs;
881882
return yield* resolveWslStartConfig({ ...shared, ...input }).pipe(
882883
Effect.provideService(DesktopEnvironment.DesktopEnvironment, environment),
883884
Effect.provideService(DesktopWslEnvironment.DesktopWslEnvironment, wslEnvironment),
884885
Effect.provideService(DesktopWslServerTree.DesktopWslServerTree, wslServerTree),
885886
Effect.provideService(FileSystem.FileSystem, fileSystem),
886887
);
887-
}).pipe(
888-
Effect.withSpan("desktop.backendConfiguration.resolveWsl", {
889-
attributes: { port: input.port, distro: input.distro ?? null },
890-
}),
891-
),
888+
},
889+
(effect, input) =>
890+
effect.pipe(
891+
Effect.withSpan("desktop.backendConfiguration.resolveWsl", {
892+
attributes: { port: input.port, distro: input.distro ?? null },
893+
}),
894+
),
895+
),
892896
});
893897
});
894898

‎apps/desktop/src/backend/DesktopBackendManager.ts‎

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1114,20 +1114,25 @@ export const makeBackendInstance = Effect.fn("makeBackendInstance")(function* (
11141114
});
11151115
});
11161116

1117-
const waitForReady = (timeout: Duration.Duration): Effect.Effect<boolean> =>
1118-
Effect.gen(function* () {
1117+
const waitForReady: (timeout: Duration.Duration) => Effect.Effect<boolean> = Effect.fn(
1118+
"DesktopBackendManager.waitForReady",
1119+
)(
1120+
function* (_timeout: Duration.Duration) {
11191121
const current = yield* Ref.get(state);
11201122
if (!current.desiredRunning) return { done: true, ready: false };
11211123
return current.ready ? { done: true, ready: true } : { done: false, ready: false };
1122-
}).pipe(
1123-
Effect.repeat({
1124-
until: (status) => status.done,
1125-
schedule: Schedule.spaced(Duration.millis(100)),
1126-
}),
1127-
Effect.map((status) => status.ready),
1128-
Effect.timeoutOption(timeout),
1129-
Effect.map(Option.getOrElse(() => false)),
1130-
);
1124+
},
1125+
(effect, timeout) =>
1126+
effect.pipe(
1127+
Effect.repeat({
1128+
until: (status) => status.done,
1129+
schedule: Schedule.spaced(Duration.millis(100)),
1130+
}),
1131+
Effect.map((status) => status.ready),
1132+
Effect.timeoutOption(timeout),
1133+
Effect.map(Option.getOrElse(() => false)),
1134+
),
1135+
);
11311136

11321137
yield* Effect.addFinalizer(() => stop());
11331138

‎apps/desktop/src/backend/DesktopBackendPool.ts‎

Lines changed: 48 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -249,61 +249,59 @@ export const layer = Layer.effect(
249249
),
250250
);
251251

252-
const unregister: DesktopBackendPool["Service"]["unregister"] = (id) =>
253-
Effect.gen(function* () {
254-
if (id === DesktopBackendManager.PRIMARY_INSTANCE_ID) {
255-
return yield* new DesktopBackendPoolCannotUnregisterPrimaryError();
256-
}
257-
const done = yield* Deferred.make<void>();
258-
const action = yield* SynchronizedRef.modifyEffect(
259-
instancesRef,
260-
(
261-
current,
262-
): Effect.Effect<
263-
readonly [UnregisterAction, ReadonlyMap<BackendInstanceId, RegisteredInstance>]
264-
> => {
265-
const entry = current.get(id);
266-
if (entry === undefined) {
267-
return Effect.succeed([{ _tag: "Absent" } as const, current] as const);
268-
}
269-
if (entry._tag === "Closing") {
270-
return Effect.succeed([
271-
{ _tag: "Wait", done: entry.done } as const,
272-
current,
273-
] as const);
274-
}
275-
const next = new Map(current);
276-
next.set(id, { _tag: "Closing", done });
277-
return Effect.succeed([
278-
{ _tag: "Close", entry } as const,
279-
next as ReadonlyMap<BackendInstanceId, RegisteredInstance>,
280-
] as const);
281-
},
282-
);
283-
284-
if (action._tag === "Absent") return;
285-
if (action._tag === "Wait") {
286-
yield* Deferred.await(action.done);
287-
return;
288-
}
289-
290-
const finish = SynchronizedRef.modifyEffect(instancesRef, (current) => {
291-
const closing = current.get(id);
292-
if (closing?._tag !== "Closing" || closing.done !== done) {
293-
return Effect.succeed([undefined, current] as const);
252+
const unregister: DesktopBackendPool["Service"]["unregister"] = Effect.fn(
253+
"DesktopBackendPool.unregister",
254+
)(function* (id) {
255+
if (id === DesktopBackendManager.PRIMARY_INSTANCE_ID) {
256+
return yield* new DesktopBackendPoolCannotUnregisterPrimaryError();
257+
}
258+
const done = yield* Deferred.make<void>();
259+
const action = yield* SynchronizedRef.modifyEffect(
260+
instancesRef,
261+
(
262+
current,
263+
): Effect.Effect<
264+
readonly [UnregisterAction, ReadonlyMap<BackendInstanceId, RegisteredInstance>]
265+
> => {
266+
const entry = current.get(id);
267+
if (entry === undefined) {
268+
return Effect.succeed([{ _tag: "Absent" } as const, current] as const);
269+
}
270+
if (entry._tag === "Closing") {
271+
return Effect.succeed([{ _tag: "Wait", done: entry.done } as const, current] as const);
294272
}
295273
const next = new Map(current);
296-
next.delete(id);
274+
next.set(id, { _tag: "Closing", done });
297275
return Effect.succeed([
298-
undefined,
276+
{ _tag: "Close", entry } as const,
299277
next as ReadonlyMap<BackendInstanceId, RegisteredInstance>,
300278
] as const);
301-
}).pipe(Effect.andThen(Deferred.succeed(done, undefined)), Effect.asVoid);
302-
yield* Option.match(action.entry.scope, {
303-
onNone: () => Effect.void,
304-
onSome: (scope) => Scope.close(scope, Exit.void).pipe(Effect.ignore),
305-
}).pipe(Effect.ensuring(finish));
306-
});
279+
},
280+
);
281+
282+
if (action._tag === "Absent") return;
283+
if (action._tag === "Wait") {
284+
yield* Deferred.await(action.done);
285+
return;
286+
}
287+
288+
const finish = SynchronizedRef.modifyEffect(instancesRef, (current) => {
289+
const closing = current.get(id);
290+
if (closing?._tag !== "Closing" || closing.done !== done) {
291+
return Effect.succeed([undefined, current] as const);
292+
}
293+
const next = new Map(current);
294+
next.delete(id);
295+
return Effect.succeed([
296+
undefined,
297+
next as ReadonlyMap<BackendInstanceId, RegisteredInstance>,
298+
] as const);
299+
}).pipe(Effect.andThen(Deferred.succeed(done, undefined)), Effect.asVoid);
300+
yield* Option.match(action.entry.scope, {
301+
onNone: () => Effect.void,
302+
onSome: (scope) => Scope.close(scope, Exit.void).pipe(Effect.ignore),
303+
}).pipe(Effect.ensuring(finish));
304+
});
307305

308306
return DesktopBackendPool.of({
309307
get: (id) =>

‎apps/desktop/src/electron/ElectronWindow.ts‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -324,8 +324,9 @@ export const make = Effect.gen(function* () {
324324
cause,
325325
}),
326326
}).pipe(Effect.orDie),
327-
sendAll: (channel, ...args) =>
328-
Effect.gen(function* () {
327+
sendAll:
328+
/* Window broadcasts run for streamed updates; avoid a span per broadcast. */
329+
Effect.fnUntraced(function* (channel, ...args) {
329330
for (const window of yield* listWindows) {
330331
if (yield* isWindowDestroyed(window)) {
331332
continue;

‎apps/desktop/src/preview/Manager.ts‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2375,8 +2375,8 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function
23752375

23762376
// The scheme is read after the session attaches so a concurrent setColorScheme is not
23772377
// overwritten with a stale snapshot.
2378-
const restoreControlSession = (tabId: string, wc: Electron.WebContents) =>
2379-
Effect.gen(function* () {
2378+
const restoreControlSession = Effect.fn("PreviewManager.restoreControlSession")(
2379+
function* (tabId: string, wc: Electron.WebContents) {
23802380
const beforeAttach = (yield* SynchronizedRef.get(tabsRef)).get(tabId);
23812381
if (beforeAttach?.webContentsId !== wc.id) return;
23822382
const control = yield* ensureControlSession(wc);
@@ -2401,7 +2401,9 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function
24012401
}),
24022402
);
24032403
}
2404-
}).pipe(Effect.ignore);
2404+
},
2405+
(effect, _tabId, _wc) => effect.pipe(Effect.ignore),
2406+
);
24052407

24062408
// Only the session that saw the detach is replaced: by the time the event is
24072409
// handled the guest may already hold a newer one, which must stay.

‎apps/desktop/src/snapShot/DesktopSnapShot.ts‎

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1708,8 +1708,8 @@ export const make = Effect.gen(function* () {
17081708
),
17091709
Effect.mapError((cause) => new DesktopSnapShotError({ operation: "list-pending", cause })),
17101710
),
1711-
read: (id) =>
1712-
Effect.gen(function* () {
1711+
read: Effect.fn("DesktopSnapShot.read")(
1712+
function* (id) {
17131713
const metadata = yield* fileSystem
17141714
.readFileString(path.join(captureDirectory, `${id}.json`))
17151715
.pipe(Effect.flatMap(decodePendingCaptureJson));
@@ -1718,11 +1718,14 @@ export const make = Effect.gen(function* () {
17181718
...metadata,
17191719
dataUrl: `data:image/png;base64,${Base64.encode(png)}`,
17201720
};
1721-
}).pipe(
1722-
Effect.mapError(
1723-
(cause) => new DesktopSnapShotError({ operation: "read", captureId: id, cause }),
1721+
},
1722+
(effect, id) =>
1723+
effect.pipe(
1724+
Effect.mapError(
1725+
(cause) => new DesktopSnapShotError({ operation: "read", captureId: id, cause }),
1726+
),
17241727
),
1725-
),
1728+
),
17261729
setAnimationDestination: (id, destination) =>
17271730
Effect.promise(async () => {
17281731
if (linuxFeedback?.id === id && destination.relativeFrame) {

0 commit comments

Comments
 (0)