Skip to content

fix(connect): protect tunnel recovery and cleanup from races

MacroscopeApp / Macroscope - Effect Service Conventions failed Aug 25, 2026 in 2m 13s

Effect Service Conventions: 4 issues found

Four convention violations were found in newly added Effect code and reported as inline review comments.

  • infra/relay/src/environments/ManagedEndpointReaper.ts (lines 81-85, 98-102): uses Effect.catchTag; tagged failures must be handled with Effect.catchTags({ ... }), even for a single tag.
  • infra/relay/src/environments/ManagedEndpointProvider.ts (lines 637-648): new Effect.catchTag in the tunnel-release path; should be Effect.catchTags({ ... }) (the module already uses catchTags elsewhere).
  • apps/server/src/server.ts (lines 641-643): startup managed-tunnel recovery wraps failures with Effect.catchCause, which also swallows interruption and reduces it to a warning; use Effect.catch to handle only the typed error channel, matching the adjacent reconcileDesiredCloudLink block.

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: changed .ts files under apps/**, packages/**, and infra/** (apps/server/src/cloud/ManagedEndpointRuntime.ts, apps/server/src/cloud/http.ts, apps/server/src/server.ts, infra/relay/src/environments/ManagedEndpointAllocations.ts, ManagedEndpointProvider.ts, new ManagedEndpointReaper.ts, infra/relay/src/http/Api.ts, infra/relay/src/worker.ts, infra/relay/src/persistence/schema.ts, infra/relay/src/deploymentConfig.ts, packages/contracts/src/relay.ts) plus the associated tests as context.

Items verified as conforming:

  • New ManagedEndpointReaper module follows the canonical layout (namespace imports from effect/* subpaths, error/result declarations, Context.Service tag with an inline interface, make, then export const layer).
  • Dependencies (RelayConfiguration, ManagedEndpointTunnelClient, ManagedEndpointAllocations, ManagedEndpointProvider) are acquired from the environment with yield* Foo.Foo; service instances are passed explicitly only in test harnesses via Layer.succeed/Layer.mock, which is an accepted test seam.
  • No ManagedRuntime.make, runPromise, or runPromiseExit introduced in service code.
  • Local service modules are imported as namespaces; @t3tools/contracts keeps named imports as expected.
  • ManagedEndpointAllocationPersistenceError extends the existing multi-value operation discriminator (enable-recovery, list-tunnels) with a message derived from structural attributes and preserves cause; no new single-value discriminators or message-only wrappers were introduced.
  • Backend behavior changes (recovery endpoint, reaper sweep, link-state serialization) are covered by focused tests using test layers rather than mocked business logic.

🔍 Agent activity — 9 tool calls

💭 I'm reviewing the ManagedEndpointReaper.ts file against Effect service conventions, checking that imports use namespace style and that the definition order follows imports, constants/interfaces, Context.Service tag, make, then layer—though I notice a helper function placed between sections that needs checking. I found a convention violation: the code uses Effect.catchTag for a single tagged error, but conventions require Effect.catchTags even for one tag. I'm spotting this pattern used twice in the deleteOrphan function within ManagedEndpointReaper.ts, and also once more in ManagedEndpointProvider.ts's release function—confirming these are all new additions in the diff. I also want to double-check whether catchTag is even the recommended API here versus catchTags, and I'm noting a minor concern about using "*" as a sentinel value for userId/environmentId in an error context, though that may be acceptable given the schema requirements. I'm also verifying that exported predicates like isManagedEndpointNotFound follow the direct schema predicate convention. Now I'm checking the semaphore setup in server.ts and the runtime, then scanning for catchTag usage and lint rules across the repo. I'm also flagging that http.ts's recoverManagedCloudTunnel maps errors without preserving the underlying cause, which conflicts with the convention of retaining the original error as cause when wrapping failures.

1. git_tools — Effect.catchTag(

2. git_tools — catchTags

💭 I notice catchTag exists in legacy code but the repo prefers catchTags, and the instructions explicitly say not to use catchTag. I'm checking the three new occurrences in this PR—ManagedEndpointProvider.ts:637 and ManagedEndpointReaper.ts:81, 98—to confirm they're newly introduced rather than pre-existing, since ManagedEndpointProvider.ts already had catchTags elsewhere at lines 356, 400, and 420. Now I want to check ManagedEndpointReaper.ts's current state to get accurate line numbers for those occurrences.

3. browse_code — infra/relay/src/environments/ManagedEndpointReaper.ts

4. browse_code — infra/relay/src/environments/ManagedEndpointProvider.ts

💭 I'm reconsidering the catchTag convention here — statically known tagged failures should use Effect.catchTags, even for a single tag, rather than catchIf with a schema predicate or catchTag. Though this handler applies a structural predicate on the underlying platform cause and re-fails otherwise, it's still fundamentally recovering a known tag, so the same convention should apply. Now I'm checking whether Effect.catch might even suffice given the error channel only contains ManagedEndpointTunnelClientError, though the convention still points to catchTags. I also want to look at the ManagedEndpointAllocationPersistenceError usage with wildcard userId/environmentId fields in listByTunnelNames to understand how that error class is structured.

5. browse_code — infra/relay/src/environments/ManagedEndpointAllocations.ts

💭 I notice the list-tunnels error uses wildcard placeholders for userId/environmentId, which feels like a minor smell but isn't explicitly forbidden by convention — I'll set this aside and focus on the more clear-cut catchTag issue instead. Looking more closely, the enableRecovery error spreads the input object which includes environmentPublicKey, a field not declared in the error schema. Since spreads bypass TypeScript's excess property checks, this could compile fine but let an unmodeled field leak into the error payload — though since it's a public key rather than a credential, it's likely too minor to flag. I want to check the recover function and error class definitions in the http.ts file to see how causes are handled.

6. browse_code — apps/server/src/cloud/http.ts

7. git_tools — class EnvironmentHttpInternalServerError

💭 These contract errors following the existing pattern with just a message field seem fine as-is. Now I'm checking server.ts's startup reconciliation logic, where the guidance says interruption should be preserved rather than downgraded to a warning—I need to verify whether the catchCause wrapper around recoverManagedCloudTunnel also inadvertently swallows interrupts, since catchCause typically catches all causes including interruption. Comparing this to nearby code that uses catch (error channel only, which preserves interruption) versus this new catchCause usage that turns interrupts into warning logs, I'm weighing whether this qualifies as the "repairs multiple independent entities" case the convention describes, since here it's just a single tunnel for a single environment. I'm also noting worker.ts has a similar pre-existing catchCause pattern for cron pruning that seems acceptable, so I need to figure out whether this server.ts instance is actually problematic given it runs both inside a forked stream loop and elsewhere at startup.

8. browse_code — apps/server/src/server.ts

💭 I notice catchCause is already an established pattern elsewhere in the code for shutdown handling, which makes me wonder whether using it here versus a plain catch could affect how interruption during shutdown gets handled—specifically whether catchCause would inadvertently swallow an interrupt signal instead of letting it propagate. Since the adjacent code uses Effect.catch for the same startup pattern, I'll flag this as a secondary finding tied to the interruption-preservation instruction, since catchCause also swallows defects and interrupts that might not be intended. Now I'm double-checking import conventions in ManagedEndpointRuntime.ts and server.ts—confirming Semaphore and Stream are imported correctly, and that the Api.ts handler's use of catchTags looks fine before moving on to check the relay HTTP adapter file. I'm reviewing recoverEnvironmentTunnelRecord's cleanup logging, function naming conventions, and checking whether the switch from Layer.provide to Layer.provideMerge in layerCloudflareBindings is an acceptable dependency exposure change for the reaper. I'm also verifying layerTunnelClient exists before reconsidering the list-tunnels error handling with wildcard userId/environmentId values against the domain-error conventions. The "*" sentinel produces a slightly misleading error message, which feels like a minor modeling issue worth a low-severity note rather than a hard finding. More concerning is that the enableRecovery error spreads input containing environmentPublicKey, a field not declared in its schema, so I want to check whether Schema.TaggedErrorClass strictly validates assigned fields or just retains extras until serialization drops them — that would make it a latent inconsistency worth flagging. I'm settling on two real findings: the three catchTag usages, and catchCause swallowing interruption during startup recovery. I've confirmed catchTags can return a failure from its handler, so now I need to pin down exact diff line numbers for inline comments — checking whether ManagedEndpointReaper.ts lines 81/98 and ManagedEndpointProvider.ts line 637 fall within the diff hunks so comments land correctly on the right side. Now I'm drafting the actual suggested replacement code for each spot, converting catchTag chains to catchTags blocks. Writing suggestion diffs... I confirm the indentation matches at line 637 and 648. Then I'm checking a fourth comment in server.ts around lines 641-643, verifying the indentation of a catchCause-to-catch change, and considering whether to include this comment given the risk that it might swallow defects unintentionally. Since the adjacent code uses Effect.catch, I'll include this as a suggestion related to interruption handling. Given the convention violations I've found, this check should default to failure status. I'm ready to post the review now.

9. modify_pr — Effect service conventions: four findings, all in newly added error-handling code (three Effect.catchTag usages and one startup Effect.catchCause). Details inline.


Agent Credits: 124 credits