Skip to content

fix(usage): show opencode tokens and models on usage page

MacroscopeApp / Macroscope - Effect Service Conventions failed Aug 31, 2026 in 5m 1s

Effect Service Conventions: 3 issues found

Three convention violations were found in the changed Effect service code and posted as inline review comments.

  • apps/server/src/usage/UsageService.ts (lines 464-470, 505): uses a newly imported node:path inside the service even though make already acquires Path.Path as path; this also forces the new file-wide // @effect-diagnostics nodeBuiltinImport:off suppression and breaks the documented invariant that raw Node filesystem/path access stays isolated in usageTranscriptReader.ts.
  • apps/server/src/usage/usageOpenCodeDb.ts (lines 34-42): reads process.env.OPENCODE_DATA_DIR / XDG_DATA_HOME from a module global, hiding an environment dependency that UsageService.make already models via HostProcessEnvironment (used there for the analogous GROK_HOME lookup). Thread the env value in from the service instead.
  • apps/server/src/provider/Layers/OpenCodeAdapter.ts (lines 1247-1265): the turn.completed payload is built as Record<string, unknown> and emitted with payload: completedPayload as never, discarding type checking against the TurnCompletedPayload contract and assigning an explicit modelUsage: undefined for an optional field. Spread only defined keys, as ClaudeAdapter.completeTurn does.

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.

Methodology: compared the PR head against the merge base for all TypeScript files in scope (apps/**, packages/**), then read the surrounding service modules to establish existing conventions: UsageService.make (acquires FileSystem, Path, ServerConfig, ServerSettingsService, HttpClient, HostProcessEnvironment), usageTranscriptReader.ts (deliberate, documented Node-fs isolation for streaming performance), packages/shared/src/hostProcess.ts (HostProcessEnvironment as a Context.Reference), packages/contracts/src/providerRuntime.ts (TurnCompletedPayload, ThreadTokenUsageSnapshot), and the Claude/Codex adapters for comparable emission sites.

Checked and not flagged:

  • Promise-returning helpers in usageOpenCodeDb.ts consumed with Effect.promise mirror the pre-existing, deliberately isolated usageTranscriptReader.ts pattern; the node:fs / node:sqlite use in that dedicated boundary module (with its own suppression comment) is consistent with the repository's existing approach.
  • No ManagedRuntime.make, runPromise, runPromiseExit, service-instance parameters, or new Layer.succeed fake-DI were introduced.
  • No new error classes, predicates, or catchTag/catchIf usage in the diff; Effect.fn span names match the surrounding file's style.
  • The pre-existing OpenCodeAdapterShape standalone interface predates this PR and was not flagged as unrelated legacy.
  • Contract/version, mobile/web provider registry, docs, and test updates are mechanical additions of the opencode provider kind and need no further changes.

🔍 Agent activity — 12 tool calls

💭 I'm scanning the changed files for Effect service convention violations, focusing on the new usage module that uses async Promise-based functions with node:fs directly, and checking whether wrapping that helper in Effect.promise within UsageService.ts counts as a violation of the expected pattern. I want to check whether the file uses the platform FileSystem service or if it suppresses the nodeBuiltinImport diagnostic file-wide instead, which would be a notable finding versus using the proper Effect abstraction. I also want to verify whether readOpenCodeDbRecords returns null on failure rather than surfacing a typed error, which would violate the convention of keeping failures typed in Effect.

1. browse_code — apps/server/src/usage/UsageService.ts

2. browse_code — apps/server/src/usage/usageOpenCodeDb.ts

💭 I'm checking whether the merge base version already had node:os imported and the diagnostics suppression comment, since the diff seems to be adding the nodeBuiltinImport:off marker fresh at the top of the file while node:os was already present before. I also want to check the existing usageTranscriptReader.ts to see if it's Effect-based with FileSystem or uses raw promise helpers, since that tells me what pattern the new module should follow.

3. browse_code — apps/server/src/usage/usageTranscriptReader.ts

4. browse_code — apps/server/src/usage/UsageService.ts

💭 I'm noticing the existing pattern keeps raw node:fs isolated to usageTranscriptReader.ts while UsageService stays on Effect's FileSystem, but this PR adds node:path/node:os imports plus a file-wide suppression to UsageService.ts, which breaks that stated invariant. I also want to check whether merge base already imported node:os without suppression, and whether the diagnostic actually tolerates certain builtins — and since UsageService already has Effect's Path service injected, using NodePath.join instead is a real convention violation worth flagging. Let me look at the surrounding UsageService code more closely to confirm.

5. browse_code — apps/server/src/usage/UsageService.ts

💭 I'm noticing UsageService.ts breaks the established convention by adding a node:path import and suppression comment when it already has access to Effect's Path and FileSystem services—the module should use path.join/path.dirname from the injected Path service instead, consistent with how usageTranscriptReader.ts isolates FileSystem usage. Checking usageOpenCodeDb.ts next: it's a new module doing filesystem and sqlite access as raw async functions that return null on failure, then wrapping them in Effect.promise—which swallows the typed errors that Effect's error model is meant to preserve. Actually the existing usageTranscriptReader already does the same null-return pattern with justified perf comments, so this new module just mirrors an existing deliberate choice rather than introducing a new issue. But it also reads process.env.OPENCODE_DATA_DIR directly, while the codebase already has a HostProcessEnvironment service used elsewhere for env access—that's a real convention violation, hiding a dependency in a module global instead of acquiring it from the Effect environment. Separately, in OpenCodeAdapter.ts I'm noticing a payload as never cast that bypasses schema typing on an emitted event, plus assigning raw OpenCode token data into a field that presumably expects a different usage shape—likely a real contract bug, though I'm unsure it falls under the specific conventions I'm supposed to check. I want to verify what the turn.completed event type actually expects before flagging this.

6. browse_code — In apps/server/src/provider/Layers/OpenCodeAdapter.ts, what is the type of the event emitted with type "turn.completed"? Show the schema/type definition of the turn.completed payload (fields like state, usage, modelUsage, totalCostUsd) from the contracts package, and how other adapters (e.g. ClaudeAdapter, CodexAdapter) build that payload. Also show the type of "thread.token-usage.updated" payload.

7. browse_code — Show the definition of HostProcessEnvironment in packages/shared/src/hostProcess.ts (is it a Context.Service?), and list places in apps/server that read process.env directly versus using HostProcessEnvironment. Is there a lint rule or convention forbidding direct process.env access in server services?

💭 I'm noticing that UsageService already relies on hostEnvironment for GROK_HOME instead of reading process.env directly, so this new code's module-global helper reading process.env may violate the convention against hiding dependencies in module globals rather than sourcing them from the injected environment. I'm also weighing whether the as never cast is a legitimate typing discipline issue: it seems to hide an incompatibility where an optional payload field is explicitly set to undefined, and casting to never bypasses type checking at a service boundary — this looks like a fair boundary typing violation worth flagging with a concrete fix. For the raw usage shape being passed through Schema.Unknown, that's acceptable since it's SDK-native and provider-agnostic. Now I'm checking whether the new NodePath usage in UsageService, along with a fresh nodeBuiltinImport:off suppression comment, is consistent with how other files in the repo handle that same lint rule — I need to grep for existing usages to confirm.

8. git_tools — nodeBuiltinImport

9. git_tools — nodeBuiltinImport|effect-language-service|languageServicePlugins

💭 I'm noticing node:os passed the lint check at merge base without suppression, which makes me wonder if the rule only flags specific builtins that have Effect platform equivalents like node:fs, node:path, or node:child_process, rather than all node imports. That confirms finding #1: UsageService.ts should use the already-acquired path service instead of importing node:path directly, dropping both the new import and the file-wide diagnostic suppression. I'm also noting that usageTranscriptReader deliberately isolates raw fs access so the rest of usage code stays on Effect FileSystem. For finding #2, usageOpenCodeDb.ts reads process.env directly instead of using the HostProcessEnvironment reference UsageService already relies on for GROK_HOME, creating a hidden module-global dependency that could break env overrides in tests — the fix would be threading env through as a parameter to resolveOpenCodeDbPath. I'm weighing whether this is too strict given the check's guidance against hiding dependencies in module globals or closures over singleton services. Finding #3 is payload: completedPayload as never, which bypasses the TurnCompletedPayload typing and explicitly sets modelUsage: undefined — a solid finding since it defeats the schema contract at a service boundary, with the fix being a typed payload built with conditional spreads similar to ClaudeAdapter. I'm also checking naming conventions for Effect.fn spans across the codebase, comparing how UsageService names its spans versus what OpenCodeAdapter uses, to see if this file's span name is consistent. I noticed filesystem access via node:fs in usageOpenCodeDb mirrors the deliberate isolation pattern in usageTranscriptReader, justified by performance, so I'm not flagging that. I also spotted that readOpenCodeDbRecords swallows failures by returning null for both unavailable sqlite and errors, causing the service to misreport a failed DB read as "ok" with skippedFiles=1 — a real correctness bug, but since it's outside the Effect conventions scope I'm checking, I'll skip it rather than flag it here.

10. git_tools — Effect.fn(

11. browse_code — apps/server/src/provider/Layers/OpenCodeAdapter.ts

💭 The Effect.fn naming looks fine, and the standalone shape interface in the adapter file is pre-existing legacy so I won't flag it. Now I'm drafting inline comments, starting with one about the NodePath.join usage and import suppression around line 464 of UsageService.ts. I'm working out the diff suggestion for replacing NodePath.join/dirname with path.join/dirname across both hunks (lines 464-470 and 505), plus removing the now-unused import and diagnostic suppression once both are converted. I need to make sure the line numbers reference the RIGHT side of the diff for the review tool. For the second file, usageOpenCodeDb.ts, I'm proposing changing the resolver function to accept an env parameter rather than reading directly from process.env, so callers pass hostEnvironment explicitly—this keeps consistency with how the codebase avoids hidden dependencies on module globals. I'll present a small diff and note the caller update needed. For the third issue, I'm looking at the payload: completedPayload as never cast in OpenCodeAdapter and preparing an inline suggestion for that line. For the OpenCodeAdapter usage/cost payload, I need to restructure the code so lastCost is read before it gets reset, building both usagePayload and costPayload upfront before the reset happens. I'm drafting the replacement lines with correct indentation to keep usage and cost captured before clearing state for the next turn. That line is too long, so I'm reformatting the ternary across multiple lines to stay under the column limit, keeping the modelUsage logic intact since it's typed as a generic record. Now I'm figuring out how to determine the final state for the findings. Since the check defaults to failure whenever there are findings, and both the node:path issue and the as never cast are clear convention violations, I'll go with failure and post the review with the three comments.

12. modify_pr — Effect service conventions review of the OpenCode usage integration. Three findings: a Node builtin used in place of the Path service already acquired by UsageService.make, a module-global process.env read for a dependency this service already models via HostProcessEnvironment, and a as never cast that removes contract typing from the turn.completed payload.


Agent Credits: 270 credits

Token usage: 56 uncached input · 966,179 cache-read input · 221,049 cache-write input · 28,058 output
Agent Credits may also include non-token charges from external tools such as web research.