Repository navigation
Effect Service Conventions: 1 issue found
apps/server/src/orchestration-v2/KeyedSerialExecutor.ts: the newThreadDispatchLockV2service exports its layer asthreadDispatchLockLayerinstead of the canonicalexport const layerfor a service-owning module. Suggested fix: move the tag and layer into a dedicated module exportinglayer(consumed via a namespace import), or rename the export tolayerin place.
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 (in scope): apps/server/src/mcp/EnvironmentMcpService.ts, apps/server/src/mcp/EnvironmentMcpService.test.ts, apps/server/src/mcp/EnvironmentPreferencesMcpService.test.ts, apps/server/src/mcp/toolkits/environment/{handlers,tools,registration.test}.ts, apps/server/src/mcp/toolkits/worktree/registration.test.ts, apps/server/src/orchestration-v2/{KeyedSerialExecutor,Orchestrator,runtimeLayer}.ts, apps/server/src/orchestration-v2/testkit/ProviderReplayHarness.ts, packages/contracts/src/environmentMcp.ts, packages/shared/src/t3McpToolPresentation*.ts.
Checked and found conforming:
- Service definition:
EnvironmentMcpServicekeeps its interface inline inContext.Service, exports a realmake, andexport const layer = Layer.effect(...); canonical single-file ordering preserved. - Dependency acquisition:
makeacquiresThreadManagementServiceandThreadDispatchLockV2from the environment withyield* Foo, so the layer's requirements stay visible; no service-instance parameters in production construction. - Runtime boundaries: no
ManagedRuntime.make,runPromise, orrunPromiseExitintroduced;threadDispatchLockLayeris composed once by reference inruntimeLayer.ts(memoized), so the MCP mutation and the orchestrator share the same lock instead of a per-feature runtime. - Test seams:
Layer.mock,Layer.succeed(ThreadDispatchLockV2, instrumentedDispatch), andEffect.provideService(...)onmakeappear only in tests/harnesses, which the conventions permit; the new tests cover real behavior (persistence allowlist, Unicode bound, concurrent downgrade), not a mechanical refactor. - Errors:
EnvironmentMcpFailureremains aSchema.TaggedErrorClasswith a structuredcodeand derived messages; the two added codes extend an existing wire-facing contract rather than introducing a new redundant discriminator, and the reworded messages track genuinely broadened semantics (capability now covers writes;environment_unavailablenow coversgetEnvironmentId). LocalSchema.isconstants match the prevailing pattern and are not wrapped in redundant functions.
🔍 Agent activity — 14 tool calls
💭 I'm planning to review the service definition and dependencies, the new dispatch lock executor, the error contract definitions, and the relevant test files to understand how these pieces fit together. I'm noticing the test file imports use an aliasing pattern that erases the module namespace, which the convention flags as problematic. I'm also examining whether the
ThreadDispatchLockV2service tag and itsthreadDispatchLockLayerreally belong insideKeyedSerialExecutor.ts, since the convention prefers a service's tag, construction, and layer to live together in a module named for that service with a canonicallayerexport — and this module's name doesn't quite match that pattern.
1. browse_code — apps/server/src/orchestration-v2/KeyedSerialExecutor.ts
2. browse_code — apps/server/src/mcp/EnvironmentMcpService.ts
💭 I'm checking whether these two
Schema.isconstants violate the convention against redundant wrapper functions — since they're private, unwrapped local constants, they seem fine, but I should verify whether exported predicates already exist elsewhere in Orchestrator.ts or ProjectionStore. ts. I also want to check ifmapErrorusing a tag predicate on a single-error-type channel is actually redundant, and whether it should instead usecatchTags, which means I need to look atThreadManagementService.getThreadProjection's signature and howOrchestratorProjectionErroris defined. Separately, I'm noting the import ofThreadManagementServicecrosses a service boundary, which the conventions have specific guidance on. Now I'm looking at named imports in the test files and flagging an explicit alias that erases the module's identity, since renaming exports likelayerandmakeviolates the naming convention. This new test file also does the same aliasing forthreadManagementLayerandmakeProviderAdapterRegistryLayer. Since the convention doesn't exempt tests and this file is in scope, I need to check whether existing test files in the repo already follow this aliasing pattern before deciding how strictly to flag it. Separately,threadDispatchLockLayerin KeyedSerialExecutor.ts is named non-canonically -- the file already exports a genericmakeKeyedSerialExecutorfactory, so adding alayerexport there feels off, suggesting the service tag and layer might belong in their own module instead. It's a legitimate finding, though I'm weighing whether it's just an optional style preference versus something worth flagging given the canonical-module convention. Checking the test cases: the mock service constructions and pipe-based environment MCP service usage look fine for tests, and adding tests for new behavior is appropriate here rather than excessive. I also verifyEnvironmentMcpFailure's error code and message switch don't violate the convention against large switches in error constructors. Since the error's discriminator-based message pattern predates this PR and is part of the existing wire contract returned to tool callers, extending it with two new codes doesn't warrant demanding a full redesign of the error class structure—flagging it would exceed the scope of concrete violations actually introduced by this change. Now I'm looking at the two updated error messages themselves. They're caller-visible since they're MCP tool failures, so the message-preservation convention applies, but the actual semantics shifted here (capability scope broadened beyond reads), so this isn't a purely mechanical rename I can dismiss outright. Moving on, I'm checking the pre-existingunavailable(...)helper, confirming production dependency wiring pulls from the environment layer correctly viayield*, and verifying thatpresentEnvironmentPreferencesis exported properly. Now I want to check whetherEffect.mapErrorwithSchema.ispredicates is actually needed by looking at the error channel forgetThreadProjection.
3. git_tools — getThreadProjection
4. git_tools — class OrchestratorProjectionError
💭 I'm weighing whether checking the error tag directly would be simpler than using a type guard here, and wondering if wrapping a private
Schema.ischeck in its own function actually counts as a violation of the redundant-wrapper rule. Since this ismapError, notcatchIf, I'm second-guessing whether the same anti-pattern applies — it might be cleaner to useEffect.catchTagsfor the known tag instead, though I'm also questioning whether validating the full schema (including the defect cause) could introduce unnecessary risk or cost. I want to check the prevailing convention for aliasing imports likelayer as xLayerin existing tests before flagging this — since these are new lines, it's fair to check but I should avoid demanding unrelated repo-wide cleanup.
5. git_tools — import { layer as
6. git_tools — Layer = Layer.|export const layer
💭 I'm noting that since every service module exports
layerby convention, and KeyedSerialExecutor.ts uniquely exportsthreadDispatchLockLayer, the newThreadDispatchLockV2service should probably live in its own module withmake/layerexports—though keeping it in KeyedSerialExecutor.ts withexport const layercould also work if it's the only layer there. But renaming would require touching six call sites, so any suggestion here would need to be a described recommendation rather than an inline diff. This feels like a moderate finding rather than a hard convention violation, so I want to keep scanning for stronger issues before deciding how much weight to give it—next I'll look more closely at the new test file and the handlers/tools code. CheckingThreadDispatchLockV2, I see its interface is defined externally rather than inline in theContext.Servicedeclaration, but sinceKeyedSerialExecutor<Key>is a genuinely reusable generic type rather than a service-specific shape, this doesn't violate the inline-interface convention. Now I'm looking at howEnvironmentMcpService.tsimports tags and errors from other modules as named imports rather than importing the service as a namespace — checking whether this fits the exception for modules used only for their tags/errors. I notice the new test file importsEnvironmentMcpServicealongside itslayerandmakeexports directly, mirroring existing patterns likeWorktreeMcpService.test.ts. Since the instructions say to treat the conventions as authoritative even when older code differs, this repeated alias-import style in new code looks like it could be flagged as a violation despite being the prevailing repo pattern. I want to check the rest of the new test file for how it constructs its service stub and provides dependencies viaEffect.provideService, since those look like legitimate test seams rather than actual issues. Yes, the layer type infers those requirements automatically. I'm now worried about a wiring risk: ifthreadDispatchLockLayergets instantiated twice instead of being memoized as the same reference across the orchestrator and MCP layer builds, the two services could end up using separate lock instances instead of sharing one in-process lock. This holds as long as the memo map is global to that build, since Effect memoizes layers by reference within a single build. The one risk would be ifLayer.freshis used somewhere or the layer gets built separately, but that seems unlikely here—they're composing the resource once via the same layer reference, which satisfies the concern about not smuggling a shared resource across multiple consumers. I want to double-check runtimeLayer.ts directly to confirm the MCP service actually receives the same lock instance.
7. browse_code — apps/server/src/orchestration-v2/runtimeLayer.ts
8. git_tools — EnvironmentMcpService
💭 I'm checking the MCP layer registration to see how ThreadManagementService and ThreadDispatchLockV2 get provided, tracing whether McpHttpServer.ts wires them from the same shared OrchestrationV2LayerLive reference so requirements bubble up correctly.
9. git_tools — threadDispatchLockLayer|ThreadDispatchLockV2
💭 I'm curious about the production wiring for the MCP server — specifically whether
ThreadDispatchLockV2is properly supplied toEnvironmentMcpService.layerin McpHttpServer, so I'm tracing where the registration requirements get satisfied through the OrchestrationV2 layer setup. Confirming that's satisfied by the memoized instance, I'm now checking other candidates: the mock service usage in registration tests looks fine, the naming convention forthreadDispatchLockLayermight warrant a note about canonicallayerexports, and I'm double-checking whether the test file's aliased imports match prevailing repo style or violate instructions. I also want to verify the namespace import style inEnvironmentMcpService.tsand whether anySchema.ispredicates are improperly wrapped rather than exported directly — so far nothing appears to be flagged there. I want to check whetherProjectionStorealready exports anisProjectionStoreThreadNotFoundError, since a local duplicate would be a minor issue, and I'm also weighing whether the new large test file is justified by proving real behavior rather than just a mechanical refactor — it seems fine. Let me look at ProjectionStore's exports to confirm.
10. git_tools — Schema.is(
11. git_tools — class ProjectionStoreThreadNotFoundError
💭 I'm noting that KeyedSerialExecutor.ts names its export
threadDispatchLockLayerinstead of the conventionallayerused across other service modules, which seems worth flagging since no exception (like an abstract port with multiple implementations) applies here. Renaming it tolayerwould force consumers to switch to namespace imports though, so there's real tension since the current descriptive name isn't unreasonable on its own. I'll flag it as a single finding pointing to the KeyedSerialExecutor.ts lines, suggesting either a dedicated module exportinglayeror keeping the export but noting the tradeoff, while staying wary of this being overly pedantic versus a genuine rule violation. Checking the rest of the diff, the domain-agnostic utility placement, dependency acquisition in EnvironmentMcpService, the mock layer usage in the test file, and the absence of ManagedRuntime/runPromise all look consistent with existing conventions, and the EnvironmentMcpFailure tagged error class in the contracts package also follows the pre-existing pattern. The switch statement in the message getter already existed before this PR, and it's only growing by two cases here, so this doesn't warrant flagging since it's modeling a code union for MCP tool wire contract errors, which is reasonable. I want to look at the full EnvironmentMcpFailure definition to see exactly how large that switch has become.
12. browse_code — packages/contracts/src/environmentMcp.ts
13. browse_code — packages/contracts/src/environmentMcp.ts
💭 This failure schema looks like it's mirroring the existing OrchestratorMcpFailure design, so I won't flag it. I'm weighing two findings: the canonical layer export naming in KeyedSerialExecutor, and some test-file alias imports that rename layer and make exports for the environment MCP module. On the aliasing, the customer's rule about namespace imports at service boundaries technically covers this since these are new lines in a new file, but the same pattern is pervasive across the repo's test files. I'm leaning toward flagging it briefly rather than not at all, since it's genuinely introduced code, but I don't want to be noisy about something that matches existing conventions—still working through whether that tension resolves toward flagging or staying silent, and separately whether the new service really warrants its own module. For the naming question, keeping the canonical module intact by renaming within KeyedSerialExecutor.ts satisfies both the "one canonical module" rule and the "export layer as
layer" convention, letting consumers import it as a namespace and referenceKeyedSerialExecutor.layercorrectly. But naming a thread dispatch lock's layer that way inside a generically-named executor module feels awkward, so I'm considering suggesting an alternative: relocatingThreadDispatchLockV2and its layer into a dedicatedThreadDispatchLock.tsfile with its ownexport const layer. I've decided to post one inline comment on KeyedSerialExecutor.ts around lines 60-69 laying out both naming options. I'm weighing whether to also flag the test alias imports as a second finding, but since the convention centers on service boundaries in production code and there's no explicit exemption or emphasis for tests, I'll leave that out to keep the review focused and credible. As for whether this single finding should actually fail the check, the layer-naming convention is explicit enough that it likely counts as a real violation, so I need to think through how the failure-bias guidance applies here given the default-to-failure stance when findings exist. Since KeyedSerialExecutor.ts lacks any canonicallayerexport while other files use secondary or alternative naming alongside a canonical one, this really is the odd file out and a legitimate deviation worth flagging. I'll draft the review with a single inline comment pointing to the new lines in the diff. Since renaming would require updating several call sites, I'll skip the suggested-diff approach and just recommend either moving the tag/layer into a dedicated module exportinglayer, or simply renaming the current export tolayerif it stays where it is. Now I'll post this as a review comment via github_pr_review with a summary body.
14. modify_pr — Effect service conventions review: one deviation found in the new ThreadDispatchLockV2 service definition. The rest of the change (dependency acquisition via yield* Foo.Foo in EnvironmentMcpService.make, Layer.effect canonical layer, inline service interface, subpath namespace imports of effect/*, test-only Layer.succeed/Layer.mock/provideService seams) matches the conventions.
Agent Credits: 154 credits
Token usage: 18 uncached input · 599,591 cache-read input · 128,350 cache-write input · 14,343 output
Agent Credits may also include non-token charges from external tools such as web research.