Repository navigation
Effect Service Conventions: 5 issues found
Reviewed the new and changed Effect service modules in scope (apps/desktop/src/electron/ElectronNotifications.ts, apps/desktop/src/ipc/methods/notifications.ts, apps/server/src/worktreeCleanup.ts, infra/relay/src/agentActivity/WebPushDeliveries.ts, infra/relay/src/agentActivity/WebPushSubscriptions.ts) plus their call sites. Five convention violations were posted as inline comments:
infra/relay/src/agentActivity/WebPushDeliveries.ts—WebPushDeliveryErrorcarries a single-valuestage: Schema.Literal("send")that duplicates the error tag and exists only to be interpolated intomessage.infra/relay/src/agentActivity/WebPushDeliveries.ts— themapErrorafterEffect.timeoutre-wraps the already structuredWebPushDeliveryErrorfromEffect.tryPromiseas thecauseof a second identical error; only theTimeoutErrorneeds translating.apps/server/src/worktreeCleanup.ts—new Error("The thread has no branch to recreate.")is manufactured solely to populate the requiredcauseof a pure validation failure;causeshould be optional and omitted there.apps/server/src/worktreeCleanup.ts— the module owns construction butmakeis not exported alongsidelayer.apps/server/src/worktreeCleanup.ts— the service interface uses an inlineimport("effect/Scope").Scopetype instead of a namespace subpath import (import * as Scope from "effect/Scope").
Details
Note
Your check run agent prompt is: .macroscope/check-run-agents/effect-service-conventions.md
More information about how Check Run Agents work can be found in our Help Center.
Checked and found compliant:
- Service definitions use
Context.Servicewith an inline interface (no standaloneFooShape), canonical file order, andFoo["Service"]for inferred member types (WorktreeCleanup["Service"]["prepareForTurn"],["start"]). - Layer constructors match the implementations:
Layer.succeedfor the synchronous Electron adapter (consistent withElectronShell/ElectronTheme),Layer.effectfor the two effectful services. - Dependencies are acquired from the environment inside
make(yield* FileSystem.FileSystem,ProjectionSnapshotQuery,TerminalManager,GitVcsDriver,WebPushSubscriptions.WebPushSubscriptions,RelayConfiguration.RelayConfiguration); no service instances are passed as constructor parameters in production code. Effect.runPromiseWithinapps/desktop/src/ipc/methods/notifications.tsis a native Electron callback boundary and matches the existing pattern inDesktopIpc.ts,DesktopWindow.ts, andDesktopApplicationMenu.ts;apps/web/src/notifications/webPush.tsusesruntime.runPromiseonly at the React/browser boundary and is not consumed by another Effect service.- Optional consumption via
Effect.serviceOption(WorktreeCleanup)inProviderCommandReactor,serverRuntimeStartup, andws.tskeeps unavailability typed rather than bypassing the layer. WebPushSubscriptionPersistenceErrorandWorktreePreparationErroruseSchema.TaggedErrorClasswith structured attributes, multi-value operation discriminators, and messages derived from those attributes.- The module-level
liveNotificationsSet inElectronNotifications.tsis documented GC-retention state, not a hidden service dependency, andmakeComponentLoggerreturns plainEffectvalues.
🔍 Agent activity — 1 tool call
💭 Looking at the new Effect service files in the diff, I see there's a new ElectronNotifications service that wraps the Electron API with Effect — it uses the standard service pattern with
Context.ServiceandLayer.succeed, and wraps the imperative Electron calls likeisSupported()andnew Notification()insideEffect.syncblocks. I'm checking whether there are hidden dependencies here — the module-level logger creation and the way the Electron API is wrapped in the layer definition. Let me compare this against how other Electron services like ElectronWindow and ElectronMenu are structured to see if there's a consistent pattern. The worktreeCleanup service looks solid — it defines an error class with a schema that captures the operation type and cause, derives the message from those attributes, and uses the standard service tag naming convention. Now looking at the import style — there's an inline dynamic import type for Scope in the service interface rather than a namespace import, which is a bit unconventional. The convention prefers namespace imports for Effect library modules, but for a standalone type used only in the interface, a named import would be acceptable. This feels like a minor style inconsistency rather than a real violation. Moving on to check the worktreeCleanup service — Semaphore is properly imported from its subpath, and the service uses the correct pattern withEffect.serviceOptionfor optional services, which aligns with the conventions. Now examining the WebPushDeliveries service in the relay infrastructure. I'm finding a real violation here: the error type has astage: Schema.Literal("send")field that's a single-value discriminator, which directly contradicts the convention against encoding the same distinction twice with both a specific error tag and a singleton literal. The message template also derives from this stage field, so I need to flag this — either drop the stage literal and derive the message from the error tag itself, or expand it to handle multiple operation values. The error does include userId and subscriptionId, which is good context. There's also a problem with how the timeout handling works:Effect.tryPromisewraps the promise, thenEffect.timeoutandEffect.mapErrorre-wrap an already-structuredWebPushDeliveryErroras the cause of anotherWebPushDeliveryError. The convention says to pass through domain errors that are already part of the declared error channel instead of double-wrapping them, so I should useEffect.catchTagsto handle this more precisely. Looking at the WebPushSubscriptions service now — it definesWebPushSubscriptionPersistenceErrorwith operation literals and derives the message from the operation, which is solid. The imports are clean, using the Drizzle ORM utilities and a namespace import for the database module. Themakefunction is exported with aLayer.effect, which is the right pattern. Now examining how the desktop notifications IPC method usesEffect.runPromise... it's running the effect with the captured context inside an Electron click callback. This is technically a native callback boundary, and since it's in an IPC handler which is an adapter layer, I think this is acceptable — the convention allows runtime execution at explicit framework boundaries like native callbacks. Moving to the ElectronNotifications module... I need to check how other Electron modules in the codebase handle imperative API calls to see if usingLayer.succeedwith direct Electron calls is the established pattern. I'm also looking atmakeComponentLoggerfrom DesktopObservability to verify whetherlogWarningreturns a pure Effect or hides a runtime dependency, and I should examine the DesktopConfig module as well.… ElectronNotifications follows the established pattern well with Layer.succeed and imperative electron calls. I'm noticing that ElectronTheme uses a namespace import for Scope from the Effect library, but worktreeCleanup deviates by using an inline import type instead — that's a minor inconsistency worth flagging against the repo's convention of importing Effect modules as namespaces. Looking at how worktreeCleanup actually uses Scope, it only appears in the type annotation.… Looking at the worktreeCleanup service now to understand its structure, error handling, and logging patterns. I notice the logging approach uses cause annotations alongside sanitized errors, which aligns with the repo's conventions. The WorktreePreparationError is being constructed with a cause, though the error message appears to be cut off mid-definition.… I'm finding an issue in the worktree cleanup code where an Error is being artificially created just to satisfy a requiredcausefield in the schema, even though this is a validation failure without an underlying error. The fix is to make thecausefield optional so we can omit it when there's no actual underlying failure—the error message already captures the relevant context from the operation and worktree path.… I'm noticing the convention in the codebase is to exportmakefunctions when a module handles its own construction, but worktreeCleanup.ts breaks this pattern with a non-exportedmake. There are a few other modules that do the same thing, though the guidance suggests exportingmakeunless it's just wrappingEffect.succeedto forceLayer.effect.… I see the issue now—the outermapErroris double-wrapping theWebPushDeliveryErrorthat already comes structured from the catch block, sincemapErroraftertimeoutencounters bothWebPushDeliveryErrorandTimeoutError. The fix is to handle these cases distinctly: either useEffect.catchTagsto give timeout its own path, or map only the timeout error while letting domain errors pass through unchanged. With a singleton stage, the natural solution combines both approaches.… Looking at the WebPushSubscriptions module, the error handling inremoveandlistForUserlooks solid. I'm also checking the worker file where it defines an async function to convert PKCS8 keys for web push, which gets wrapped with Effect.promise at the API boundary—that's fine. There's some duplication of base64 decoding helpers between web and worker code, but that's not a concern. Now I need to examine the web notifications module. The web push adapter usesruntime.runPromiseat the React boundary, which is acceptable since it's an imperative bridge between Effect and the browser's Promise API—it's not a dependency of another Effect service. The app also uses the atom registry pattern for accessing the relay session, which is consistent with existing conventions. Checking the desktop IPC notifications handler, it follows the same pattern withEffect.runPromiseWithat the native callback boundary, so the approach is consistent across platforms. For the server side,ProviderCommandReactorusesEffect.serviceOption(WorktreeCleanup)with the optional service layer properly merged into the runtime. The import pattern is slightly inconsistent though—some files use namespace imports likeimport * as WorktreeCleanupwhile the reactor uses a named import of the service tag, which diverges from the convention of importing the service module as a namespace at service boundaries. The convention's main concern is around layer/make renaming, and while consuming service tags via named imports is common throughout the codebase, I won't flag it as that would require repository-wide cleanup. Looking at the actual violations: WebPushDeliveries has a singleton literal that duplicates the error tag distinction, plus it's double-wrapping an already-structured error after the timeout operation. And worktreeCleanup is manufacturing an Error object just to fill the required cause field. For the remaining issues, worktreeCleanup doesn't export its make function (the module controls construction), and it uses an inline Scope import instead of the namespace import pattern I see elsewhere like in ElectronTheme. I'm deciding whether to post four separate comments or combine the last two into one focused note, since they're on different lines anyway. I need to target the actual diff line numbers when posting to the PR—WebPushDeliveries.ts line 33 for the stage singleton violation would be my first comment. I need to decide whether to include both themakeexport and Scope import comments, and I'm leaning toward including both since they're explicitly stated conventions. For thestageremoval fix, I could propose replacing it with a more specific schema that constrains the values to "send" or "timeout", which would address multiple issues at once while also fixing the message line. I could dropstageentirely and derive the message from the tag instead, or keep it as a real multi-value discriminator covering both "send" and "timeout" paths. The cleaner approach might be to handle timeout errors separately by only mappingTimeoutErrorinstances, but if I want to preserve the distinction, I'd makestageaccept both values and adjust the message template accordingly. I'll suggest replacing the entire block with a version that either removes the redundantstagefield or expands it to be a genuine discriminator. Refining the error handling to catch only TimeoutError and let WebPushDeliveryError propagate naturally. Then for the worktreeCleanup function, I'm making the cause field optional in the schema so it can be omitted when there's no underlying error to attach.…
1. modify_pr — Effect service conventions review of the new/changed service modules (ElectronNotifications, worktreeCleanup, WebPushDeliveries, WebPushSubscriptions, IPC method). Four findings, all in error modelling / module shape; service definitions, dependency acquisition, and layer composition otherwise follow the conventions.
Agent Credits: 265 credits