Repository navigation
Effect Service Conventions: 3 issues found
Three error-modeling/handling convention violations were found in the new Pi provider code. Inline comments were posted.
apps/server/src/textGeneration/PiTextGeneration.ts:100—cause: String(cause)stringifies the underlyingPiRpcError, erasing the error chain; pass the failure itself ascause.apps/server/src/textGeneration/PiTextGeneration.ts:207-214— usesEffect.catchTag("TimeoutError", ...); statically known tagged failures must be handled withEffect.catchTags({ ... }).apps/server/src/provider/pi/PiRpcClient.ts:225(with the class at lines 40-43) —PiRpcProcessExitedErrorhas nocausefield and itsdetailis built fromString(cause), so the immediate underlying failure and stack are dropped and the message is opaque defect text.
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.
Scope reviewed: new and changed TypeScript under apps/**, packages/** for this PR — apps/server/src/provider/Drivers/PiDriver.ts, apps/server/src/provider/Layers/PiAdapter.ts, apps/server/src/provider/Layers/PiProvider.ts, apps/server/src/provider/pi/{PiRpcClient,PiRpcSchema,PiModel,PiSessionFile}.ts, apps/server/src/textGeneration/PiTextGeneration.ts, apps/server/src/provider/builtInDrivers.ts, packages/contracts/src/{settings,providerRuntime}.ts, and the web/mobile registration call sites.
Checked and found acceptable:
- Namespace imports from
effect/*subpaths throughout the new modules; named imports retained only for@t3tools/contracts, pure helpers, schemas, andeffect/unstable/process, matching the existing provider modules. - Dependency acquisition:
makePiAdapter,makePiTextGeneration, andcheckPiProviderStatusacquireCrypto,FileSystem,Path, andChildProcessSpawnerfrom the Effect environment; the optionalmakeRpcClientfactory andonSessionPublishedhook are legitimate test seams/callback strategies, and noManagedRuntime.make,runPromise, orrunPromiseExitwas introduced in production code (Effect.runSyncappears only in test fakes with a documented lint suppression). - No standalone
FooShapetypes, compatibility re-export shims, or generic error-constructor aliases were introduced;PiDriverfollows the existingProviderDriverregistration pattern andBUILT_IN_DRIVERS/BuiltInDriversEnvwere updated mechanically. - Error classes use
Schema.TaggedErrorClasswith structured attributes (command/requestId,operation/sessionFile,provider/method), and predicates are exported/derived viaSchema.is.
Not flagged (repository precedent, outside these conventions): the detail: cause.detail ?? String(cause) normalization in PiAdapter.request mirrors existing adapters (OpenClawAdapter, CodexAdapter), and the Pi model discovery failed: ${String(error)} provider snapshot message in PiProvider.ts is a probe status string rather than an Effect error attribute.
🔍 Agent activity — 1 tool call
💭 I'm reviewing the diff to check for Effect service convention violations across several key files. Looking at the imports first—they're using namespace imports from Effect subpaths, which seems correct, though I notice one import is using named imports from the unstable process module instead of a namespace import, which might be inconsistent with the convention. For the error types, I'm seeing some issues.
PiRpcProtocolErrorhas adetailfield that's a static string like "failed to spawn Pi RPC", andPiRpcCommandErrorpulls its detail from the wire response withresponse.error ?? "Pi RPC command failed". The convention warns against copying raw wire payloads into detail, so that's borderline problematic. More concerning isPiRpcProcessExitedError—it's stringifying the cause directly into the detail field and dropping the cause entirely, which violates the rule about preserving the underlying error as cause and not duplicating it in the message. InPiProvider.ts, there's a message likePi model discovery failed: ${String(error)}that looks user-facing, so I should check how other providers handle similar messages. ForPiSessionFileError, it's structured with operation and sessionFile fields, which is good, but it's usingcause: Schema.Defect()to store a raw value rather than an actual error—that's a borderline case of manufacturing an error just to populate cause. TheEffect.fnusage looks fine, and dependency acquisition inmakePiAdapterproperly pulls Crypto, FileSystem, Path, and ChildProcessSpawner from the environment. ThemakeRpcClientoption serves as a test seam, which is appropriate. InPiDriver.ts, the pattern of acquiring the spawner from the environment and then re-providing it viaEffect.provideServiceis consistent with what other drivers do. ThemakePiTextGenerationfunction defaultsprocess.envas a parameter, which reads a module global—acceptable since existing providers follow the same approach. However, I'm noticing a convention violation withEffect.catchTaginPiTextGeneration.ts. The codebase guidelines say to useEffect.catchTagswith an object for all tagged error handling, even single tags, and to avoidcatchTagentirely. The current code usesEffect.catchTag("TimeoutError", ...)when it should beEffect.catchTags({ TimeoutError: () => ... }). TheEffect.catchinPiAdapteris fine since it's handling the whole channel. For the tests,Effect.runSyncwithQueue.unboundedis acceptable since it's in test code with a lint suppression comment explaining the rationale. I should verify whether the repo already usescatchTagelsewhere to understand the current state of compliance. I'm also noticing thatPiRpcProcessExitedErrorandTextGenerationErrorboth violate the error handling conventions — they're stringifying the cause instead of preserving it as the actual error object, which loses the original error information and breaks the pattern of deriving messages only from stable attributes. I'm noticing that TextGenerationError uses a shared contract with operation/detail/cause fields, where the detail field is the standard shape. The current I'm spotting a pattern of convention violations in the codebase. There's a use ofcatchTagin PiTextGeneration.ts at line 207 that directly contradicts the stated convention against it. Additionally, around line 120 in the error mapping for makeRpcClient, the code is stringifying thecausevalue when it should preserve the original underlying value to maintain the error chain—other similar code in the repo passescausedirectly without transformation. Looking at the adapter conventions, I see that PiAdapter'srequest()helper converts the cause into a detail string when one isn't explicitly provided, which aligns with the existing pattern of usingcause.message. But there's a tension here—the convention warns against adding a detail field that just copiescause.messageand then using that to build the wrapper message, yetProviderAdapterRequestErrorseems to do exactly that, which technically violates the guideline even though it's a widespread pattern in the codebase. TheString(cause)fallback in detail is particularly problematic because it can pull in arbitrary error text, which directly contradicts the instruction not to copy arbitrary defect text into detail. I should flag the clearest violations:Effect.catchTagshould beEffect.catchTagsin PiTextGeneration around line 207, thecause: String(cause)pattern in PiTextGeneration erases the error chain, andPiRpcProcessExitedErrorlacks acausefield while constructing detail fromString(cause), which violates both the "preserve the immediate underlying error" and "don't merely stringify an opaque cause" rules. Let me check the exact lines in PiRpcClient.ts and PiTextGeneration.ts to comment accurately and see if there are other violations likePiRpcProtocolError. ForPiRpcCommandError, the detail comes from a wire payload (response.error), which is borderline — the convention warns against copying raw wire payloads into detail, but similar patterns exist elsewhere in the codebase, so I'll skip that.PiSessionFileErroris trickier: it usescause: Schema.Defect()withcause: input.instanceId, storing a raw value rather than an error, which is a mild misuse since pure validation errors without underlying failures don't need a cause, and the invalid value should probably be a structural field instead. Theoperationfield uses a multi-value discriminator ("instanceId", "containment", "symlink", etc.), which the convention allows with a generic message, and sincePiSessionFileErrorhas no message override, the default is fine. Checking the rest:PiAdapterhandles the channel withEffect.catchcorrectly, noManagedRuntimeorrunPromisein production code, and the namespace imports aren't alphabetically ordered but that's not a convention here. These modules are factory functions matching existing patterns, so noContext.Serviceis needed, andPiDrivermatches theProviderDriverstructure. For dependency acquisition,makePiTextGenerationtakes settings, environment (defaulting toprocess.env), and amakeRpcClientfactory parameter, which matches the existing si I've identified three specific issues in the error handling: the first is in PiTextGeneration where converting the cause to a string breaks the error chain—it should stay as the raw cause object. The second is that the same file needs to switch fromEffect.catchTagtoEffect.catchTagswith a TimeoutError handler. The third problem is in PiRpcClient where the error gets stringified into a detail field and the underlying cause is lost entirely, but the error class definition doesn't even have a cause field to store it. For the fourth issue in PiAdapter, therequest()helper is copying stringified cause details into the detail field, which violates the convention against duplicating cause information into wrapper messages. The helper itself is borderline—it does some normalization by pickingcause.detail, but that normalization is exactly the discouraged pattern. I think it's worth flagging as a lower-priority concern since existing code already does similar things withcause.message. I'm noticing a tension between the provider probe message being user-facing text and the rule against splicing arbitrary error details into user-visible messages. The test expects the message to start with "Pi model discovery failed:", but usingString(error)could leak raw wire error strings or other implementation details that shouldn't be exposed to callers, which violates the convention about keeping direct error attributes and log annotations bounded and safe. I'll focus on the three solid findings rather than flag this as a violation—it's a weaker concern compared to the concrete cases. Now I need to map out the exact line numbers for inline comments in the diff: the first issue is at line 100 in PiTextGeneration.ts where the cause is being stringified, the second spans lines 207-214 in the same file for the catchTag block, and the third is at line 225 in PiRpcClient.ts, with a note about the missing cause field around lines 40-43.
1. modify_pr — Reviewed the new Pi provider modules (PiDriver, PiAdapter, PiProvider, PiRpcClient, PiSessionFile, PiTextGeneration) against the Effect service conventions. Dependency acquisition, namespace imports, and layer/factory shapes look consistent with the existing provider modules. Three error-modeling/handling violations are noted inline.
Agent Credits: 194 credits