Skip to content

fix(server): preserve bootstrap welcome ordering

MacroscopeApp / Macroscope - Effect Service Conventions failed Aug 1, 2026 in 3m 15s

Effect Service Conventions: 3 issues found

Three findings posted as inline review comments:

  • apps/server/src/cloud/serviceLauncherClient.ts — the launcher client is a real service (Effect-returning operations plus IPC state) but is declared as a Context.Reference with a fake "unmanaged" default and no exported layer, so consumers (selfUpdate.make, serverRuntimeStartup.make, persistence/Layers/Sqlite.ts layerConfig) never surface the dependency and silently fall back to trial: false / no-op prepareTrial when the layer is not wired.
  • apps/server/src/cloud/pinnedRuntime.ts — the invariant comment explaining the module-global install semaphore was deleted while the semaphore itself remains.
  • apps/server/src/cloud/bootService.ts — the file-wide @effect-diagnostics nodeBuiltinImport:off suppression is now dead; the file has no node:* imports since the writes moved to FileSystem/Path.

Previously flagged items that now look resolved: namespace imports for local service modules (Sqlite.ts, selfUpdate.ts), FileSystem/Path acquisition for the boot-service administrative writes, the dedicated PinnedRuntimePreflightBlockedError tag instead of overloading step, structured operation discriminator plus preserved cause on the launcher client error, and single construction of the launcher client in server.ts.

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.

Files examined at head 74e600f: apps/server/src/cloud/serviceLauncherClient.ts, serviceProtocol.ts, servicePreflight.ts, bootService.ts, pinnedRuntime.ts, selfUpdate.ts, apps/server/src/serverActivation.ts, serverRuntimeStartup.ts, server.ts, persistence/Layers/Sqlite.ts, cli/servicePreflight.ts, plus packages/shared/src/hostProcess.ts and packages/contracts/src/server.ts for context.

Notes on items deliberately not flagged:

  • ServiceLauncherHostProcess as a Context.Reference defaulting to global process matches the established @t3tools/shared/hostProcess pattern in this repo.
  • ensurePinnedRuntimeInstalled taking fs/path/runner parameters predates this PR; only the validate callback (a deliberate strategy) was added.
  • serviceLauncher.ts restricting itself to node:* imports is required by its standalone bundle constraint and is documented in the file header.
  • Named imports from effect/unstable/cli in cli/servicePreflight.ts match every other CLI module in the repo.
  • Terse comments in bootService.ts (renderBootServiceUnit invariants) were flagged in an earlier run and are not reposted.

🔍 Agent activity — 1 tool call

💭 I'm reviewing the current state of the PR to see what's changed since the previous runs flagged issues. Looking through the key files involved in the service launcher, boot process, self-update logic, runtime management, and database persistence to understand what needs attention. Looking at the current issues, ServiceLauncherClientError is using a multi-value discriminator with operation and a free-form reason string as its only message source, but the convention requires structured attributes with Schema.TaggedErrorClass and deriving the message from those attributes rather than storing unstructured text. However, the author already addressed a prior flag by adding the operation discriminator while keeping reason to pass through the launcher's rejection reason to the user as caller-visible behavior, so this seems acceptable now and doesn't warrant re-flagging. Moving on to check selfUpdate.ts and verify that Sqlite.ts is now using the namespace import correctly—the diff confirms it's importing ServiceLauncherClient as expected. Now I need to examine whether ServiceLauncherClient as a Context.Reference with a defaultValue containing a failing requestUpdate is the right pattern, since the conventions recommend using Context.Service with an inline interface instead. The defaultValue here is pure, but ServiceLauncherHostProcess is also a Context.Reference whose defaultValue reads the global process object—that's a runtime-backed default, which the conventions warn against hiding in module globals or Layer.succeed implementations. This mirrors the pattern in @t3tools/shared/hostProcess though, so I should check that file to see if this is an established convention. I also need to look at the decode functions in serviceProtocol.ts to see if they're hand-rolled instead of using Schema, which would trigger the preferSchemaOverJson lint diagnostic. Since serviceLauncher.ts is a standalone bundle with only Node builtins, the hand-rolled decoders are justified. Now I'm moving on to check selfUpdate.ts, serverRuntimeStartup, and the server.ts sections. ServiceLauncherClient should follow the same pattern as ServiceLauncherHostProcess — it needs to be a Context.Reference with a default implementation. The module exports make but is missing the canonical layer export that the convention requires, so I need to check whether that's intentional or if it should be added. The memoization behavior within a single Layer build means their explicit sharing pattern is actually defensible—providing the same layer twice would memoize anyway. Given that the convention explicitly calls for exporting a canonical layer, it's a judgment call, but I'll skip flagging it since the prior run already caught the double-provide issue and the author intentionally removed it. Now I'm looking at whether the Context.Reference default value for ServiceLauncherClient is hiding a dependency—using a fallback default means consumers like the Sqlite layer and serverRuntimeStartup never declare it in their requirements, which could mask a missing dependency. I should check whether this pattern of using Context.Reference for services with Effect methods is actually established elsewhere in the codebase before flagging it as a violation. The codebase uses Context.Reference for small values and seams, but ServiceLauncherClient as a Reference with a full fake default is a new pattern—it's a service with Effect-returning methods and IPC state. I'm wondering if this breaks convention since the standard approach is Context.Service with its inline interface, make, and layer, and using a Reference means the dependency silently defaults without explicit app configuration. The default behavior might be intentional though—it represents the "unmanaged server" state when the server runs standalone rather than spawned by the launcher. But make already handles that case by returning managed:false when the context env var is absent, so the default duplicates existing logic. Since make is always called and provided in server.ts, the Reference default only activates when tests or other entry points forget to include the launcher layer. Looking at the actual usage patterns—layerWithOptions in serverRuntimeStartup, tests using ServerRuntimeStartup.layer, and Sqlite.layerConfig in tests—they all rely on this default as a fallback for the "not launcher-managed" scenario. This feels like deliberate design rather than a clear violation, similar to how ServerActivation defaults to undefined. Given that other parts of the codebase use References with sensible defaults (like ClaudeExecutableFileCheck performing real filesystem checks), I'm hesitant to flag this as a hard violation. Let me look at other areas for clearer issues—like servicePreflight.ts, which appears to be a pure sync function using node:sqlite directly and running from a CLI command without proper Effect representation. The real concern here isn't just the convention about hiding dependencies; it's that runServicePreflight opens a sqlite database synchronously and swallows all errors into a generic "database update" message, conflating missing files with corruption. That's more of a behavior and error-handling issue than a convention violation. Now I need to check the ProviderCommandReactor and server.ts changes for any hidden runtime dependencies or globals, particularly in serverRuntimeStartup.ts. I'm checking the imports in the modified files to catch any convention violations—looking for improper named imports from "effect", local service imports that should use namespace syntax, and similar issues. Let me start by examining the current state of pinnedRuntime.ts. Looking at the module-global semaphore that controls the pinned runtime install lock — it's a pre-existing mechanism that was already in place, though this PR removed the explanatory comment about how it serializes the check/install/sentinel transaction across boot-service setup and remote self-update callers in the same process. This is worth flagging as a violation of the "preserve useful comments" principle, especially since a similar timeout comment in pinnedRuntime was previously identified and restored. The lock's rationale documentation was deleted while the synchronization mechanism itself remains, leaving future maintainers without context for why this global serialization exists. On the second point, PinnedRuntimeInstallInput is receiving fs, path, and runner as explicit constructor parameters rather than acquiring them from the Effect environment using the standard yield* Service.Service pattern that production code should follow. The remaining changes look sound: the validate callback is intentional, PinnedRuntimePreflightBlockedError properly separates the error into a distinct tagged type with version and reason fields as requested, and the error handling on fs.rename covers the entire error channel appropriately. Now I'm moving to examine the behavior change in ProviderCommandReactor.ts where Effect.interrupt replaces Effect.failCause(cause), and then checking how reconcileServ is being used in the service layer. flight gets invoked during effect construction rather than during execution, which means the synchronous side effects run immediately instead of being wrapped in Effect.sync. While the practical difference is minimal since the handler is called once per CLI invocation, it still violates the principle of keeping side effects explicit within the Effect system. The bigger issue is that servicePreflight.ts performs synchronous database I/O using node:sqlite directly and silently catches all errors instead of using Effect's error handling. It's not really a service in the conventional sense — more of a standalone helper — but it's also not constrained like serviceLauncher.ts is. Since it runs in the CLI environment where Effect's FileSystem and SqlClient are available, using them would be more aligned with the conventions. Now looking at the actual findings: ServiceLauncherClient is defined as a Context.Reference with a fake default implementation rather than a proper Context.Service with a layer, which effectively hides the dependency from consumers like Sqlite.layerConfig and serverRuntimeStartup.make — they never declare it in their requirements and just silently fall back to unmanaged or no-op behavior. There's also no exported layer, so consumers end up hand-rolling Layer.succeed instead. This feels like the strongest convention violation here. The question is whether this counts as a concrete violation worth flagging. The convention calls for Context.Service with an inline interface, make, and layer, but a Reference with a default serves a real purpose — many entry points like tests, desktop, and CLI serve don't actually need a launcher context. Still, for a service with Effect-returning operations and real IPC state where a silent default changes behavior (migrations run in a trial child if the layer is missing), I think it's fair and actionable to flag: define it as Context.Service and export the layer, then provide it explicitly where an unmanaged fallback is actually needed. I'll add this as a comment phrased as a suggestion rather than a hard violation. On the bootService file, I'm checking whether the earlier flagged comments about missing documentation were restored. The current lines 36, 56, 58 are still terse, and since the author didn't restore those docs in the previous run, I won't repost the same duplicate comment. The pinnedRuntime lock rationale is a separate deletion that wasn't flagged before—it's a legitimate preservation issue for a module-global semaphore whose necessity isn't obvious, so I'll flag it briefly. Now looking at selfUpdate.ts, there's a pattern where the code derives a wrapper message from another error's free-form field, which was already flagged in an earlier run. The current version still uses error.reason as the wrapper message for the rejected case, which is intentional pass-through of the launcher-visible reason to the user. Since this was partially addressed before and passing through the launcher rejection reason is a caller-visible message requirement, I won't repost it. The same applies to failWith(error.reason, error) for PinnedRuntimePreflightBlockedError—that's also an intentional pass-through of the user-facing blocked message. Now checking the new ServerUpdateTerminalError in packages/client-runtime/src/state/server.ts with its attributes for targetVersion, status, and reason. The message is derived correctly. Moving on to verify the selfUpdate.ts layer setup and then examining the CLI imports in servicePreflight.ts to see if the pattern of importing from effect/unstable/cli is consistent across other CLI files. The sqlite.ts file is working now. I'm weighing whether the ServiceLauncherClient Reference-vs-Service mismatch is a critical enough issue to fail the validation. The risk is real—if the dependency isn't properly wired in some code path, migrations could silently run in an unmanaged trial child, corrupting data, while prepareTrial would silently no-op and cause the launcher to time out. This hidden default behavior violates the principle of making dependencies explicit. Looking at the pinnedRuntime changes, the lock comment was removed but the lock itself remains. The removePinnedRuntimeInstallation function is gone entirely, which is fine. There's a minor issue where the docstring for ensurePinnedRuntimeInstalled now sits above the PinnedRuntimeInstallInput interface instead of the function itself—a documentation placement problem from the refactor. I'll flag both the Reference-vs-Service concern and this doc misalignment as suggestions. For bootService.ts, there's a leftover diagnostic suppression at the top (@effect-diagnostics nodeBuiltinImport:off) that's no longer needed since all imports are from effect/ and @t3tools packages, not node builtins. That's worth noting as a small cleanup. I'm also scanning through serviceLauncherClient.ts to check for unused error literals and unnecessary Effect.die calls, but those look fine. Given the findings so far—the Reference vs Service inconsistency, the lost comment, and the dead suppression—these are moderate enough to warrant a failure. The instruction says findings lead to failure unless there's clear guidance otherwise, so I'll mark this as failed and draft comments with proper line numbers for the changed files. For the serviceLauncherClient.ts issue at line 73, the real problem is that it's declared as a Context.Reference with a fake unmanaged default and exports no layer, so consumers silently fall back to no-op behavior if the layer is missing—in a trial child that means migrations run when they shouldn't. I should suggest switching to Context.Service and exporting a proper layer... For pinnedRuntime.ts line 21, I need to check what the diff actually shows around that semaphore declaration and restore the lock rationale comment that was removed. I'm worried that if one comment fails, the entire review submission could fail, so I should attach the second comment to a line that's definitely in the diff. Looking at the context, line 15 was changed and should be safe, but I need to verify the line numbers carefully—the deletion spans around lines 20-22 in the old file, and the context after includes the new lines with const pinnedRuntimeInstallLock and other declarations. For the third comment about the stale diagnostic directive in bootService.ts at line 1, it's a small but concrete fix and worth including since that line is in the diff. Let me write these out.

1. modify_pr — Reviewed the Effect service changes in this PR (launcher client, boot service, pinned runtime, activation/startup wiring, persistence layer). Prior findings on namespace imports, Path/FileSystem acquisition in bootService, the blocked-preflight error tag, and structured error context all look addressed. Three remaining items below.


Agent Credits: 196 credits