Repository navigation
Effect Service Conventions: 1 issue found
apps/server/src/serverSingleton.ts—LiveLegacyServerRuntimeis an exported intermediate error whose only use is a single-use 1:1catchTagsmapper inacquireServerSingletonthat renames its fields intoServerAlreadyRunningError(and recomputes a pathacquireLockalready holds). ConstructServerAlreadyRunningErrorat the failure boundary instead, matching the sibling refusal path in the same function.
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.
Reviewed files in scope: apps/server/src/serverSingleton.ts, apps/server/src/serverSingleton.test.ts, apps/server/src/server.ts.
Checked and found conforming:
- Namespace subpath imports (
effect/Effect,effect/Layer,effect/Schema, …); no consolidated named imports from"effect"; local modules imported as namespaces at the service boundary (ServerSingleton.*,ServerConfig.*). - Service failures use
Schema.TaggedErrorClasswith structured attributes;messageis derived from those attributes only (never fromcause/cause.message), andServerLockUnavailableErrorkeeps the realcausewith a multi-valueoperationdiscriminator (open/lock), so no redundant singleton discriminator.Data.TaggedErrorfor internal control-flow errors matches established repository usage. - No raw payloads, credentials, or defect text copied into error attributes or log annotations.
- Known tagged failures are handled with
Effect.catchTags;Effect.catchis used only where the whole error channel is intentionally handled, and the platformNotFoundcheck is a genuinely structural predicate. - Dependencies (
FileSystem,Path,Crypto) are acquired from the Effect environment; no service-instance parameters,ManagedRuntime.make,runPromise/runPromiseExit, or hidden module-global runtimes.processIsAliveWithis a deliberate callback seam, not service injection.Layer.effectDiscardordering inserver.tskeeps the claim structural rather than relying on a global. - New behavior (lock acquisition/release, stale-lock recovery, port metadata, legacy-runtime refusal) is covered by focused tests rather than mocks of core logic; no new lint/type suppression directives were introduced.
🔍 Agent activity — 9 tool calls
💭 I'm reviewing the new serverSingleton.ts file against the conventions, checking that it uses proper subpath namespace imports and that its tagged errors like ServerAlreadyRunningError and ServerLockUnavailableError follow the expected schema pattern with derived messages. I'm checking whether the operation field with "open"/"lock" values counts as a legitimate multi-value discriminator, whether Schema.Defect() for cause is structurally sound rather than just stringifying the underlying error, and whether the internal control-flow errors using Data.TaggedError should instead follow the Schema.TaggedErrorClass convention, plus verifying that the isServerLockBusyError predicate is appropriately typed. I'm also looking at the catch/catchTags patterns in readHolder and beginExclusiveWrite to confirm they're handling the whole error channel intentionally rather than misusing catchIf, and checking whether the new test file is justified since it covers genuinely new behavior rather than a mechanical refactor. Now I'm comparing the two construction sites for ServerAlreadyRunningError to see if they diverge inconsistently. I'm noting that the lockPath field in the legacy case points to server-runtime.json, which feels slightly misleading semantically but isn't a real convention violation since tests confirm the message includes the right filename constant. I'm also checking whether ServerLockUnavailableError properly requires a cause field, which it does. Now I'm looking at processIsAlive, which calls process.kill directly at the module level in an imperative way, and I'm considering whether that counts as a hidden dependency that should be injected instead. I'm also reviewing openLockDatabase's use of dynamic import wrapped in Effect.try, and checking the import ordering in the file—Data before Crypto is alphabetically off but not worth flagging. Then I'm examining whether readHolder should use Effect.catchTags instead of Effect.catch when handling a PlatformError with a structural predicate, though conventions seem to permit catchIf here, so it's not a clear violation. I want to check the actual repo for similar existing lock modules to confirm these patterns. Now I'm verifying whether
writeFileStringAtomicallyandreadPersistedServerRuntimeStateactually exist, and examining howServerSingletonLiveusesLayer.effectDiscardwith a scoped acquireRelease effect—confirming that Layer.effect's type signature excludes Scope from the remaining requirements since the layer's build scope satisfies it. I'm also checking whether this codebase is on Effect v4 given the presence ofeffect/Crypto,Schema.TaggedErrorClass, andEffect.fn, and looking at the repo's conventions doc to see if it prefersSchema.TaggedErrorClassoverData.TaggedErrorfor service failures. I noticeLiveLegacyServerRuntimeis exported but seems to be used only as an internal signal converted into aServerAlreadyRunningError. I'm now wondering if the intermediate error is actually unnecessary — sinceacquireLockcould fail withServerAlreadyRunningErrordirectly instead of creating a separate error class that gets mapped 1:1 throughcatchTags, since this extra layer doesn't seem to carry any additional information. Constructing errors away from the failure boundary seems like a moderate violation worth flagging, but I want to check the actual repository code to confirm this before committing to that judgment.
1. browse_code — apps/server/src/*.ts
2. browse_code — .macroscope/*
💭 I want to check serverRuntimeState.ts and atomicWrite.ts for existing patterns, then look at how server.ts implements the related changes.
3. browse_code — apps/server/src/serverRuntimeState.ts
4. browse_code — apps/server/src/atomicWrite.ts
💭 I'm noting that the temp directory prefix used in writeFileStringAtomically doesn't quite match what the test checks for leftover files, but that looks like a test correctness issue rather than something in scope for the Effect service conventions I'm reviewing. Let me move on and look at the full committed serverSingleton.ts and the relevant section of server.ts. Now I'm thinking harder about actual convention violations. First, checking whether the repo commonly uses Data.TaggedError versus the recommended Schema.TaggedErrorClass pattern for service failures. Second, I'm looking at ServerLockAttemptError, which wraps a cause only to be immediately unwrapped one line later in beginExclusiveWrite -- this feels like unnecessary indirection similar to the "helper that just constructs an error" anti-pattern, though it's a class rather than a function, so I'm weighing whether that distinction actually matters here. Now I'm examining LiveLegacyServerRuntime's exported error class more closely -- it's built with Data.TaggedError and gets translated one-to-one into ServerAlreadyRunningError via catchTags in acquireServerSingleton, duplicating path computation that acquireLock already has access to. This looks like a stronger case of an intermediate error existing only to be remapped at the caller rather than being constructed at the actual failure boundary, which the conventions explicitly discourage. Then I check whether the module needs a Context.Service -- it doesn't, since it's just plain functions using environment resources like FileSystem, Path, and Crypto. I also look at ServerSingletonLive using Layer.effectDiscard for acquisition-only behavior, comparing it against the convention of exporting canonical layers, and it seems consistent with the existing style in server.ts alongside other similar layers. I check error attribute safety and confirm nothing raw gets stringified into messages, then note that callback injection like processIsAliveWith is an allowed deliberate strategy. I review readHolder's error handling logic and conclude that using Effect.catch to recover from a NotFound case is fine, since catchIf is meant for structural predicates rather than this kind of scenario. I also weigh whether ServerAlreadyRunningError needs to capture the underlying cause, deciding it doesn't since it's purely a domain condition. Then I look at ServerLockUnavailableError's use of Schema.String for the operation field versus the repo's usual pattern of Schema.Literals, wondering if this looseness counts as a minor convention deviation worth flagging. Using Effect.ignore on recordServerLockPort to swallow errors isn't a listed convention concern. No lint suppressions were added, and catchTags for a single tag looks correct. I want to verify the Crypto module and randomUUIDv4 usage against existing repo patterns, and check if there's a docs/effect convention file by searching for Data.TaggedError and Crypto.Crypto usages.
5. git_tools — Data.TaggedError
6. git_tools — randomUUIDv4|Crypto.Crypto
💭 Since Data.TaggedError is commonly used across the codebase, that's not a concerning pattern. I want to cross-check the serverSingleton.ts file against head to confirm the diff's line numbers line up, then look closer at whether openLockDatabase's use of process.versions.bun and dynamic imports inside an Effect.try creates any hidden runtime boundary issues worth flagging. I'm noticing Effect.ignore swallowing certain lock errors, but that's outside my rule set. The stronger candidate is
LiveLegacyServerRuntimemapping an intermediate error toServerAlreadyRunningErrorvia catchTags in the same module — this looks like pure indirection that should be constructed directly at the failure boundary inacquireLock, since that function already has the state paths needed to build the error itself, making the intermediate mapping unnecessary. I'm also weighing whetherServerLockAttemptErrorinbeginExclusiveWriteis a similar no-op wrapper, since it wraps a cause only to unwrap it a line later — though this feels more like defensible internal plumbing, so I might fold it into a single combined comment rather than flag separately. Then I'm looking at whetheroperation: Schema.StringonServerLockUnavailableErrorshould instead be a bounded literal type to match the discriminator pattern used elsewhere in the app. I decide this is a style preference rather than a real violation, so I'll keep it as at most a minor inline note and stay focused on the strongest finding. Now I'm digging into howServerSingletonLive, an effect-discard layer producing no services, interacts withLayer.provideordering when piped intoHttpServerLive. I confirm thatLayer.providealways builds the provided dependency layer before building the layer it feeds into, even if that dependency layer contributes nothing to the output context — Effect builds it regardless of whether its output is consumed, and memoization guarantees it only runs once, so the ordering guarantee holds here. The lock scope tied to the layer build lasts for the app's entire lifetime, which is expected. I also note thatHttpServerLivemight be provided elsewhere without the singleton wrapper, but that's outside what I'm checking, and in server.ts,Effect.ignoreis already swallowing the lock-unavailable error appropriately. I don't see hidden-dependency layer issues, andisServerLockBusyErrorfits the structural predicate convention fine. The test file is a bit heavy usingmakeServerLayerdirectly, but nothing here violates conventions against unnecessary tests. So I'm landing on essentially one finding to flag, and need to decide whether it's severe enough to fail the review. If there truly are no findings, the instructions want the entire response to be exactly "All clear," which conflicts with the JSON schema requirement — likely the intent is that a no-findings case maps to a success state with that title. But I do have a candidate finding: a trivial error-constructor helper that's just wrapping arguments into a new error, possibly used once with mapError, which the conventions explicitly discourage. The LiveLegacyServerRuntime case looks like a solid concrete finding — an internal error class plus a single-use catchTags mapper renaming fields into ServerAlreadyRunningError with no real normalization, and there's also duplicated computation of the state path. But I want to check whether this pattern might be intentional, since acquireLock already constructs ServerAlreadyRunningError directly in its busy branch, so I'm comparing whether the legacy case's mapper is truly redundant or serves a distinct purpose. I want to confirm this is a real inconsistency within the same function, then check the actual file to get correct line numbers for my inline comments.
7. browse_code — apps/server/src/serverSingleton.ts
💭 This confirms LiveLegacyServerRuntime is just a single-use internal error that gets translated 1:1 into ServerAlreadyRunningError, even though acquireLock already builds that error elsewhere and has the needed state. I'm leaning toward constructing ServerAlreadyRunningError directly at line 247 and removing the class and catchTags entirely, but I want to double check what readPersistedServerRuntimeState actually returns first. I'm also double-checking that LiveLegacyServerRuntime isn't referenced anywhere else, including tests. Since the fix spans multiple locations (removing the class, changing line 247, removing catchTags), I should present it as a full replacement rather than a single unified diff hunk. I'm also weighing whether ServerLockAttemptError deserves the same flag — it's similar in spirit but does provide a typed channel for the Effect.try, so I'm inclined to leave it alone to keep the review focused. Now I'm thinking about whether to mark the check as failure or neutral: the violation is real but modest, and since the check defaults to failure when findings exist, I need to weigh that against the "may fail" language in the instructions. Given the guidance to default to failure with a concrete finding, and since this pattern (an intermediate error class plus a catchTags mapper) is analogous to the flagged anti-pattern in spirit even if not literal, I'll treat it as a violation worth failing on. I'll post the inline comment explaining the concern on the class definition lines, referencing the "construct at the failure boundary" principle.
8. git_tools — LiveLegacyServerRuntime
💭 This confirms it's exported but only used internally with a simple one-to-one mapping, so I'll post the review with a single inline comment.
9. modify_pr — </antml="">
Agent Credits: 11 credits
Token usage: 4 uncached input · 121,496 cache-read input · 622 cache-write input · 1,470 output
Agent Credits may also include non-token charges from external tools such as web research.