Skip to content

fix(teleport): restore presence when export fails after pending

MacroscopeApp / Macroscope - Effect Service Conventions failed Aug 17, 2026 in 2m 49s

Effect Service Conventions: 2 issues found

Two error-modeling convention violations in the new teleport code:

  • packages/contracts/src/teleport.ts — TeleportFileLockedError and TeleportLockProbeError store message as a schema field even though every construction site passes an identical template over nativePath; message should be derived from the structural attributes via a getter (same pattern applies to TeleportSchemaVersionError and TeleportIdentityConflictError).
  • apps/server/src/teleport/fileLock.ts — lockProbeError is a helper whose only behavior is constructing TeleportLockProbeError with a constant message; the error should be constructed at each failure boundary instead.

Both findings were posted as inline review comments with suggested fixes.

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.

Verified as compliant in this revision:

  • apps/server/src/teleport/TeleportService.ts is a single canonical module ordered imports → tag (Context.Service with the interface declared inline) → make → layer; dependencies are acquired via yield* from the environment and appear in the layer's requirements. No ManagedRuntime.make / runPromise in service code.
  • apps/server/src/teleport/formats/registry.ts now exposes the adapter set as a Context.Service with make/layer instead of a mutable module global populated by import side effects.
  • fileLock.ts runs the lsof probe through the ProcessRunner service, so the process dependency is represented in the Effect environment and layerable in tests.
  • Tagged failures are recovered with Effect.catchTags (no catchTag/predicate-based catchIf for known tags); Effect.catch is used only where the whole error channel is intentionally handled.
  • Effect library modules are imported as subpath namespaces; local service modules used at boundaries (OrchestrationEngine, ProjectionSnapshotQuery, TeleportFormatRegistry, ProcessRunner, ServerSettings) use namespace imports, and named imports are limited to @t3tools/contracts, pure helpers, and schemas.
  • ws.ts / server.ts / server.test.ts consume the service through the namespaced module shape (TeleportService.TeleportService, TeleportService.layer).

🔍 Agent activity — 1 tool call

💭 I'm reviewing the code changes against the conventions that were flagged in previous runs. Most of the issues have been resolved—fileLock now uses ProcessRunner, the registry is properly set up as a Context.Service, error handling includes message getters, and the TeleportService uses the correct module structure. Now I'm checking what's left to verify, starting with how TeleportService.ts implements its layer setup. I'm also examining the import patterns across the codebase, particularly around service boundaries. There's a question about whether ProviderInstanceRegistry and similar services are being imported consistently—some files use namespace imports while others use named imports, and I need to check if this aligns with the established convention for service boundaries. Looking at formats/registry.ts, the make function uses Effect.sync wrapped in Layer.effect, which feels like it might be forcing the layer pattern unnecessarily. The convention suggests using Layer.succeed for pure synchronous values instead, so this could be a minor violation worth noting. Moving to the error definitions in contracts/teleport.ts, I'm seeing that several error classes store unstructured message fields directly as data. The convention calls for using Schema.TaggedErrorClass with structured attributes and deriving the message from those rather than storing it raw. TeleportUnsupportedProviderError has provider and message, TeleportSchemaVersionError has provider, nativePath, foundVersion, supportedVersion, and message, and there are more errors following this pattern. The author fixed some of these with message getters in the prior run, but left message fields on others. While the convention says "rather than storing an unstructured message as the only data," here the message isn't the only data—there are structured attributes alongside it. However, the messages are stored rather than derived, and they're often constructed with identical templates across multiple files like fileLock.ts and nativeWrite.ts. For instance, TeleportFileLockedError always formats as Native session file is locked: ${nativePath} and TeleportLockProbeError as Failed to check whether ${nativePath} is locked. These are derivable from the structured fields, which suggests a plausible violation worth flagging. This is new code in teleport.ts, and the prior run already flagged the message-as-data issue at line 137, which was partially addressed for TeleportInvalidInputError, TeleportProjectResolutionError, and TeleportDiscoveryError. The remaining ones—FileLocked, LockProbe, NativeWrite—still store message fields. Though NativeWriteError has varying messages that do discriminating work ("Failed to create directory for X", "Failed to write temp session file", etc.), which arguably should be structured as operation/stage fields instead, that's a broader design critique that might be risky to push. Now I'm checking whether TeleportFileLockedError and TeleportLockProbeError each have just one message template per site, and verifying the imports—like the namespace import of ProcessRunner in fileLock.ts and the HostProcessPlatform service tag from the shared package subpath, which aligns with the convention for service modules. I'm noticing that both TeleportFileLockedError and TeleportLockProbeError construct their messages the same way each time they're instantiated, so they should use a getter to derive the message from nativePath instead. There's also a lockProbeError helper in fileLock.ts that violates the guideline against creating helpers that only wrap error construction with a constant message—it should be removed since it's just adding boilerplate without meaningful behavior. I'll add a comment to the error class in packages/contracts/src/teleport.ts about implementing message getters and mention the unnecessary helper in fileLock.ts, since both files are within scope. Now I need to check how other services consume ProviderService to understand the import convention at service boundaries — whether TeleportService should import it as a namespace or individually. Named imports for service tags are standard practice, so that's not worth flagging. Looking at TeleportService.ts now, I see inFlight is a mutable Set created inside the make closure rather than at module scope, which is acceptable—though using a Ref would be more idiomatic by convention. The error mapping throughout uses catchTags and inline Effect.mapError calls to construct typed errors, which is solid. Effect.catch at boundaries is fine when the entire error channel is handled. The pure helper files like discovery.ts, homes.ts, and cwd.ts look good, and the web-side teleport.ts is also pure. For type predicates, I see hand-written guards like isTeleportProvider rather than Schema.is wrappers, which is acceptable. TeleportSchemaVersionError has a structured message field plus attributes like provider, nativePath, and foundVersion. Looking at the error classes more closely: SchemaVersionError, IdentityConflictError, and others derive their messages directly from those structured attributes in codex.ts and claude.ts — the messages are templates of existing fields. NativeWriteError holds varied messages across different operations, which arguably should be a stage discriminator instead. The pattern I'm flagging is stored message fields that are pure templates of existing attributes — those should use override get message() instead. This feels like a concrete convention violation worth noting, since the guidance explicitly says to derive messages from attributes rather than storing unstructured text. But I need to weigh this carefully: the rule targets cases where message is the only data, and here we have nativePath, message, and cause. The message is redundant duplication, which is a style point rather than a hard violation. The author already addressed the reason-only cases in the prior run, and the check defaults to failure. I should look for stronger findings elsewhere before failing on a residual style preference — let me examine the full TeleportService.ts and registry.ts files for any hidden dependencies or more concrete issues. I'm identifying code violations in the codebase. The lockProbeError helper in fileLock.ts is a problematic pattern—it only constructs an error object, which violates the convention against helpers that exist solely for error instantiation. I should refactor this by moving the message derivation logic into a getter within TeleportLockProbeError itself and constructing it inline instead. This addresses two specific violations that I'll need to flag when making the contracts change. Looking at related error classes, TeleportFileLockedError and TeleportLockProbeError both store messages that are pure templates derived from their fields—these should use override get message() getters instead. I'm checking whether TeleportSchemaVersionError and TeleportIdentityConflictError follow the same pattern. After scanning the construction sites in codex.ts and claude.ts, I can see that SchemaVersionError's message is derivable from the provider, format version, and file path fields. IdentityConflictError's message is also derivable from the session ID and project context. However, NativeWriteError is different—its message carries operation-specific information that isn't captured elsewhere, so flagging it would require adding a new field to track the operation stage, which aligns with the convention about capturing stable domain context. For the registry.ts issue, the make function uses Effect.sync wrapped in Layer.effect, which is the pattern the convention warns against—forcing Layer.effect just to export a make function when the construction is pure. I could suggest using Layer.succeed directly with the pure value instead, though I recognize that keeping make exported isn't inherently wrong since the module owns its construction logic. Looking at TeleportService.ts, the make function correctly exposes its dependencies through the effect's R type—it requires FileSystem, Path, TeleportFormatRegistry, and ProcessRunner, with the layer providing the latter two and the others coming from the environment, which is fine. Now examining the alreadyInFlightError() helper at line 204... it's a zero-argument error factory that just constructs a TeleportInvalidInputError, which technically matches the pattern of helpers that shouldn't exist—ones whose only job is wrapping error construction. It's used twice in the module, so it's borderline; I could flag it alongside similar violations, but I'm inclined to keep the focus elsewhere. The bigger issue is that TeleportInvalidInputError is still being used as a catch-all for semantically distinct failures—session busy, password mismatch, dispatch failures—and the prior review flagged this at line 137 without structural changes. The convention is to split these into separate error classes when a reason field drives message selection or control flow. Here, reason is free-form text that is the message itself, not a discriminator, so it's really just renaming the message field rather than properly distinguishing error types. The code did change in response to the last feedback—they added a message getter—but the core problem (unstructured reason as the only data) persists. Since the change was a direct response to what was flagged, posting another comment would be redundant, even though the fundamental redesign (splitting into TeleportSessionBusyError, TeleportCwdMismatchError, etc.) still hasn't happened. I think the best approach is to focus on concrete, actionable issues in the new code: the stored message template fields and the lockProbeError construction-only helper. Since packages/contracts/src/teleport.ts is entirely new in this PR, everything there is fair game. I'll flag the TeleportFileLockedError and TeleportLockProbeError blocks where message is a fixed template derived from nativePath at every construction site—I can suggest deriving it with an override getter instead and dropping the stored field. For the second comment, I'll point to the helper in fileLock.ts whose only purpose is constructing that error. I should also mention that the same pattern applies to TeleportSchemaVersionError and TeleportIdentityConflictError, whose messages are similarly pure templates over existing fields. Now I need to identify the exact diff line numbers on the RIGHT side where these comments should be posted. I'm reviewing the error classes in the teleport module and noticing that both TeleportFileLockedError and TeleportLockProbeError store a message field that's always constructed the same way from nativePath. I should suggest refactoring these to compute the message via a getter instead, eliminating the redundant stored field, and I'll provide inline suggestions for both class definitions. However, I'm realizing this change isn't fully self-contained—it would require updating the call sites in fileLock.ts, nativeWrite.ts, and the test file where these errors are decoded with the message field. Since the fix touches code outside the diff lines, I can't use a simple inline suggestion and should instead present this as a unified diff showing the complete change needed. There are real convention violations here, so I'll mark this as a failure state and post the review.

1. modify_pr — Two error-modeling convention issues in the new teleport code: message stored as data when it is a fixed template over existing attributes, and a helper whose only behavior is constructing that error.


Agent Credits: 195 credits