Skip to content

fix(server): distinguish legacy runtime ownership

MacroscopeApp / Macroscope - Effect Service Conventions failed Sep 1, 2026 in 1m 29s

Effect Service Conventions: 1 issue found

  • apps/server/src/serverSingleton.ts (lines 77-84, 320-334): ServerAlreadyRunningError now uses the optional legacyRuntimeStatePath attribute as a discriminator that selects the user-facing message tail, and the construction site stores the same path twice (lockPath and legacyRuntimeStatePath) to trigger that branch. Per the error conventions, a discriminator that drives the user-facing message should be split into separate error classes (a dedicated legacy-runtime error, failed directly from acquireLock).

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 the two new/changed in-scope files (apps/server/src/serverSingleton.ts, apps/server/src/serverSingleton.test.ts) and the touched wiring in apps/server/src/server.ts.

Checked and found acceptable:

  • Namespace subpath imports from effect/*; named imports in the test for pure helpers/constants of a non-service module; ServerConfig imported as a namespace.
  • No Context.Service/make/layer service module is introduced or moved, so the service-definition and file-layout rules do not apply; ServerSingletonLive in server.ts is a Layer.effectDiscard at the application composition boundary, not a hidden runtime.
  • No ManagedRuntime.make, runPromise, or runPromiseExit added; processIsAliveWith takes a deliberate callback strategy, not a service instance.
  • ServerLockUnavailableError derives its message from structural attributes (operation, lockPath) and preserves the underlying failure as cause; operation is a genuine multi-value discriminator with a generic derived message.
  • Effect.catchTags used for the known tagged failure; Effect.catch on the platform error is a full-channel handler.
  • New tests cover new backend behavior (not a mechanical refactor), so they are expected.

The prior run's finding about the LiveLegacyServerRuntime \u2192 catchTags rename hop was not re-posted separately; the suggested fix for the message-discriminator issue removes that hop as well.


🔍 Agent activity — 5 tool calls

💭 I want to check whether the previously flagged issue with the catchTags mapper in LiveLegacyServerRuntime still exists at the current head commit, so I'll fetch that file to compare against the earlier flagged version.

1. browse_code — apps/server/src/serverSingleton.ts

2. git_tools

💭 The code seems unchanged from what was previously flagged, though the redundancy comment was tied to an earlier commit. I want to verify whether the file actually changed between that commit and the current head, so I'll check the PR's commit history.

3. github_api_read_only — repos/{owner}/{repo}/pulls/{pull_number}/commits

💭 I'm checking whether the head commit fixing legacy runtime ownership addresses the prior comment, so I want to diff it against the earlier commit to confirm.

4. git_tools — 0197d8e..70b3f7d

💭 The author's fix adds a distinguishing field (legacyRuntimeStatePath) rather than removing the intermediate error, so the mapper now carries some real info instead of pure renaming. But the underlying issue I flagged—that the intermediate error exists solely to be renamed without actual normalization—still isn't fully addressed. I'm weighing whether this counts as a genuine response to my earlier comment or just a partial dodge. Since the code did change meaningfully in that direction, even if marginally, I should treat it as a response rather than re-flagging the same violation outright. But now I'm noticing a fresh issue: the optional legacyRuntimeStatePath field acts as a discriminator that picks between two distinct user-facing messages, which is exactly the pattern the conventions say should be split into separate error classes rather than branched inside one message getter. I also notice lockPath gets overloaded in the legacy case—it's set to the same value as legacyRuntimeStatePath, duplicating that field. That redundancy is a concrete tell that two distinct error shapes are being crammed into one class. I'm also checking ServerLockUnavailableError's operation field, which uses Schema.String for what should really be a literal union of "open"/"lock" values. Then there's ServerLockAttemptError, a private error used only within beginExclusiveWrite to smuggle a cause out of an Effect.try before being caught and converted to a plain result object—essentially a throwaway wrapper whose only job is passing the cause through. Similarly, LiveLegacyServerRuntime is a Data.TaggedError that's exported but functions purely as internal control flow, always caught by acquireServerSingleton before it can escape the public API, so I'm not flagging it despite being exported. Now checking import ordering—the Data and Crypto imports aren't alphabetized, though that's a minor style nit. Looking at server.ts, Layer.effectDiscard for ServerSingletonLive looks fine since it's not a service module. In the test file, the imports check out too: named imports from serverSingleton.ts are acceptable since that module exports pure helpers and constants rather than being a Context.Service module, and server.ts consistently imports it as a namespace. I'm also keeping in mind the guidance not to add large tests just to validate a mechanical refactor. Since this is genuinely new behavior rather than a refactor, tests are warranted here. I'm also checking readHolder's use of Effect.catch on a PlatformError against the NotFound tag—since it handles the whole channel intentionally, that fits the convention. The processIsAliveWith(pid, sendSignal) callback injection is also explicitly allowed as a deliberate strategy. That leaves the main candidate finding around the optional legacyRuntimeStatePath discriminator and its branching logic. This is significant enough to flag, since the convention calls for splitting into separate error classes when a discriminator drives the user-facing message, and it's newly introduced in this commit. The fix would span multiple hunks—defining a dedicated error like LegacyServerAlreadyRunningError with its own fields and message, removing the optional field and branch, and letting acquireServerSingleton skip the remapping—so I can't produce a clean diff, but I'll describe the suggested fix, which also resolves the earlier finding. I want to confirm this ServerAlreadyRunningError is entirely new to this PR so I know the full scope is in play. Since serverSingleton.ts is a new file, every line is part of the diff, so I'll reference lines 77-84 on the RIGHT side for my comment on the message getter branch, briefly noting the duplicated lockPath as well. Given the convention explicitly calls this out, I'll treat it as a clear enough violation to fail the check, and I'll keep the comment concise.

5. modify_pr — Submitted review with 1 inline comment


Agent Credits: 62 credits

Token usage: 10 uncached input · 183,829 cache-read input · 58,821 cache-write input · 5,081 output
Agent Credits may also include non-token charges from external tools such as web research.