Repository navigation
Effect Service Conventions: 1 issue found
apps/server/src/usage/UsageService.ts— the new subscription-limits code insideUsageService.makereads the process environment from theprocess.envmodule global (lines 171, 217, 275, 284) instead of theHostProcessEnvironmentreference the same constructor already acquires (hostEnvironment, line 154, used at line 387). Dependencies should come from the Effect environment rather than a module global, otherwise a provided/isolated environment no longer reaches the dev fixture flag or the Claude/Codex subprocess probes.
Two inline suggestions were posted with the fix.
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 areas and findings:
- Imports and namespaces: new modules use namespace subpath imports (
effect/Effect,effect/Fiber,effect/Semaphore,effect/Scope,effect/unstable/process/ChildProcessSpawner); no consolidated named imports from"effect".@t3tools/contractsand the pure helper moduleusageSubscriptionLimits.tscorrectly use named imports. - Service definition:
UsageServicekeeps its inlineContext.Serviceinterface, realmake, andexport const layer; onlysubscriptionLimits: []was added tolayerTest. NoFooShapetype or compatibility shim introduced. - Dependency acquisition/runtime boundaries:
ChildProcessSpawner,FileSystem,Path,ServerSettingsServiceand the newSemaphore/Scoperesources are acquired withyield*inmake;Effect.provideServiceis used to satisfy sub-effect requirements from those already-acquired instances (consistent with the existing comment at lines 502-503). NoManagedRuntime.make,runPromise, orrunPromiseExitwas added, and no service instances are passed into production constructors. The only violation is the directprocess.envreads noted above. probeCodexRateLimits/probeClaudeUsage(CodexProvider.ts, ClaudeProvider.ts): mirror the existing sibling probes (probeClaudeCapabilities,probeCodexAppServerProvider,checkGrokProviderStatus) including theirenvironment = process.envdefault parameters, so these were treated as consistent with existing repo patterns rather than new violations.- Errors and predicates: no new
Schema.TaggedErrorClass, discriminator, orSchema.ispredicate was introduced; existingUsageReadErrorusage is unchanged. - File layout: no service module was moved or split; no re-export shims added.
- Change discipline: behavior change is backed by focused tests (
usageSubscriptionLimits.test.ts,usageTranscriptReader.test.ts,usageMerge.test.ts,usageFormat.test.ts,UsagePage.test.tsx), including contract-v4 decode defaults and the read-budget timing paths. - Web/mobile files in scope contain React presentation only, with no Effect service usage.
🔍 Agent activity — 17 tool calls
💭 I'm scanning the diff for Effect service convention issues, checking whether the new imports of Effect, Semaphore, and Scope use proper namespace subpaths, and they look fine so far. Now I'm moving on to check the subscription-related service file next. Turning to
probeClaudeUsage, I'm checking the dependency acquisition pattern wherefileSystem,path, andChildProcessSpawnerare pulled from the environment viayield*and then explicitly provided to a sub-effect—this seems acceptable. Then I'm examining the error-handling signature on the ClaudeProvider probe function, which declaresneveras its error channel despite internally usingEffect.tryPromiseand calls that could fail, piped throughEffect.resultto capture outcomes safely. I also note no new error classes were introduced, so error conventions are mostly not applicable here, and theLayer.succeedtest fixture only had asubscriptionLimits: []field added. I'm now wondering whether direct use ofprocess.envinside the service'smakefunction violates the convention against hiding dependencies in module globals—though this looks like a pre-existing pattern in the codebase rather than something new introduced by this diff, so I want to check how the repo typically handles environment variable access within Effect services. I want to verify import conventions in the actual repo files, checking whether they use named imports from "effect" consistently. Let me check the imports in UsageService.ts specifically.
1. browse_code — apps/server/src/usage/UsageService.ts
2. browse_code — apps/server/src/usage/usageSubscriptionLimits.ts
💭 I notice the new code reaches for
process.envdirectly in a few places rather than going through the existingHostProcessEnvironmentservice that's already available ashostEnvironmentinmake. That's inconsistent since there's an Effect service specifically meant to abstract environment access. I want to check whathostEnvironmentactually exposes and whether existing UsageService code already relies on it for similar reads.
3. git_tools — hostEnvironment|process.env
4. browse_code — What is HostProcessEnvironment in packages/shared/src/hostProcess.ts? Show its definition (Context.Service?), its interface members, and how server code obtains the process environment through it (e.g. environment field). Also list examples where callers pass process.env directly instead.
💭 I notice
hostEnvironmentis already acquired at line 387 inmake, yet the new code bypasses it and readsprocess.envdirectly at four sites instead—exactly the kind of hidden dependency the guidelines warn against, since production code should acquire env values from the yielded service rather than global process state. This also breaks test overrides that rely on injectinghostEnvironment. I should check whether other provider probe call sites elsewhere in the repo passprocess.envorhostEnvironmentwhen invoking things likeprobeClaudeCapabilitiesorprobeCodexAppServerProvider, to see if this pattern is consistent or an outlier.
5. git_tools — HostProcessEnvironment
6. git_tools — probeClaudeCapabilities|probeCodexAppServerProvider|probeCodexRateLimits|probeClaudeUsage
💭 I notice AccountLimitsService.ts exists at the PR head but isn't part of the diff, which means it must already exist in the merge base on main. That's curious since it seems like a usage-limits service that predates this PR—I should check that further. Now I want to look at how ClaudeDriver handles passing environment variables into probes around line 163, since that shows the local convention for this pattern.
7. browse_code — apps/server/src/provider/Drivers/ClaudeDriver.ts
8. browse_code — apps/server/src/usage/AccountLimitsService.ts
💭 I'm confused why the file wasn't found at the PR head tree when the search found it—possibly the search defaulted to main HEAD rather than the reviewed commit, since main may have moved ahead. I'll set that discrepancy aside as it's not central to the task. Let me focus on real convention issues in the diff. I'm flagging that
UsageService.makereadsprocess.envdirectly in several places even thoughHostProcessEnvironmentis already yielded ashostEnvironmentin the same scope—this bypasses proper dependency injection and should be fixed to usehostEnvironmentinstead, since it hurts testability. I'm also looking atprobeClaudeUsagein ClaudeProvider.ts, where anAbortControlleris created outside the Effect context in the function body. I'm considering whether that abort controller could get reused improperly if the effect runs multiple times, comparing it against the existingprobeClaudeCapabilitiespattern to see if this concern is already handled elsewhere in the codebase.
9. browse_code — apps/server/src/provider/Layers/ClaudeProvider.ts
💭 The probeClaudeUsage pattern matches the existing convention, so that's fine. Now I'm checking CodexProvider's new probeCodexRateLimits and startCodexAppServerProbe for how they handle scope and resources, and whether Effect.scoped is being misused there.
10. browse_code — apps/server/src/provider/Layers/CodexProvider.ts
💭 I'm noticing the probe function defaults to reading process.env and process.cwd() directly, which raises the question of how existing callers like the app server provider supply their environment—I should check how similar patterns elsewhere in the file already handle this before deciding.
11. git_tools — process.env|process.cwd
12. git_tools — process.env
💭 Using process.env as a default parameter matches existing conventions like GrokProvider, so that part is fine. But it's inconsistent that UsageService.make passes process.env explicitly when hostEnvironment is already available in scope - I should check how the Grok home env is handled for comparison. I see it deliberately uses hostEnvironment instead, confirming this inconsistency is worth flagging: swap process.env for hostEnvironment at those four sites. This falls within the dependency-boundary check's scope since reading process.env directly bypasses the existing HostProcessEnvironment abstraction already available in the same function. Now I want to look more carefully at another area - whether UsageService.make's scope requirement from Effect.addFinalizer is compatible with how the layer constructs it, since adding a finalizer means the constructor needs a Scope.
13. browse_code — apps/server/src/usage/UsageService.ts
14. git_tools — export const layer
💭 I notice the line numbers don't match between the two grep results, which makes me suspect the grep defaulted to the main branch rather than the PR head commit referenced by the tree ref. I also see that AccountLimitsService.ts appeared in an earlier grep but doesn't exist at the PR head, which reinforces that git_grep might be searching a different, later revision of main — yet the exact process.env line matches at 171/217/275/284 are hard to explain by coincidence. Let me just directly check the head version of UsageService near its end, and confirm how Layer.effect handles Scope requirements in this repo's Effect v4 setup.
15. browse_code — apps/server/src/usage/UsageService.ts
💭 I'm checking whether Layer.effect in Effect v4 can handle an effect that requires a Scope for finalizers, since the service constructor now uses addFinalizer. Then I'm scanning through other candidate convention violations: the contracts file's namespace import for Effect.succeed looks fine, the UsageService's use of Effect.provideService to inject already-acquired FileSystem/Path/ChildProcessSpawner instances is an intentional pattern (there's even an explanatory comment in the file), and the Semaphore usage looks standard too. I'm also checking whether swallowing errors via catchCause in the settings and refresh paths violates the interruption-preservation convention, but that guidance is specifically about startup reconciliation, and this pattern is already used consistently elsewhere in the file, so it seems fine. The subscription limits module is just pure helpers, not a service, so named imports from it are appropriate rather than a namespace import. I'm also verifying that
Effect.fnusage on the probe/await helpers is fine since they live in a non-service module, that the timeout budget passed as a plain number works fine as milliseconds, and that there's no hidden runtime or unwrapped promise leaking outside Effect—the Node reader is properly wrapped inEffect.promise. The one real finding I'm settling on is thatUsageServicereadsprocess.envdirectly instead of going through theHostProcessEnvironmentdependency the codebase already models and that this same function already yields—that's a legitimate, narrow violation of the "don't hide dependencies in module globals" convention. There's tension between the literal instruction to output exactly "All clear" and the schema requiring valid JSON — I'll resolve it by using "All clear" as the summary content within the required JSON structure if things end up clean. Now I need to decide whether there's actually a finding worth flagging, and I should also check the mobile/web files for similar issues before finalizing. Looking closer at UsageService, I notice it deliberately routes environment reads through HostProcessEnvironment (like the GROK_HOME lookup), yet the new code adds four direct process.env reads within the same service — bypassing that abstraction exactly where it matters, since it makes dev-fixture and provider-probe reads unoverridable in test contexts. That's a concrete, defensible violation of the "don't hide dependencies in module globals" principle. I also see a similar pattern in CodexProvider.ts's probeCodexRateLimits function, which defaults its environment and cwd parameters directly from process global state. I'll consolidate my feedback into a single inline comment covering the affected lines rather than scattering multiple comments, focusing on the probe call sites where this pattern is clearest and easiest to act on. I notice line 387 uses bracket access on hostEnvironment rather than dot notation, likely because of a noPropertyAccessFromIndexSignature rule in tsconfig — so I should suggest bracket-style access for consistency and verify that's actually enforced.
16. git_tools — noPropertyAccessFromIndexSignature
💭 Dot access works fine since that env var isn't a reserved word, so that check passes. I'm also confirming that the hostEnvironment binding on line 154 already existed before this change, which supports my finding that the reference is already bound and the new code bypasses it. Now I'm drafting the actual review comments: one flagging that
makeSubscriptionLimitsDevFixturereadsprocess.envdirectly at lines 171 and 217 instead of using the already-boundhostEnvironment, with inline suggestions matching the exact indentation at each location. The second comment covers lines 275 and 284, whereprobeClaudeUsageandprobeCodexRateLimitsare passedprocess.envdirectly rather thanhostEnvironment— I'll comment on 275 with a suggestion and reference 284 alongside it. For severity, since production behavior is unaffected (both default toprocess.env), this is really a convention/testability issue rather than a functional bug, so I'm weighing whether it should still count as a failure given it violates the emphasized dependency-acquisition pattern.
17. modify_pr — Effect service conventions: one finding — the new subscription-limits code inside UsageService.make reads the process environment from the module global process.env instead of the HostProcessEnvironment reference the same constructor already binds (hostEnvironment, line 154, used at line 387). Everything else in the diff (namespace subpath imports, make/layer shape, Context.Service tag, dependency acquisition via yield*, no new runtimes or runPromise, no new error classes) matches the conventions.
Agent Credits: 228 credits