Skip to content

fix(desktop): unsubscribe deep-link renderers so route-unmount does n…

MacroscopeApp / Macroscope - Effect Service Conventions failed Aug 26, 2026 in 4m 7s

Effect Service Conventions: 2 issues found

Two convention violations found, both in apps/desktop/src/app/DesktopDeepLink.ts:

  • Hidden dependency in a module global (lines 75-107): earlyCapture is process-wide mutable module state holding the Electron app singleton, and the service's configure consumes it via drainEarlyOpenUrls(), so the early-capture buffer is an invisible input to the service and shared across tests. Suggested fix: return the capture handle from captureEarlyOpenUrls in main.ts and inject it as a plain value (e.g. a Context.Reference provided with Layer.succeed).
  • Ambient environment read instead of environment acquisition (line 279): process.argv is read directly inside the service, although @t3tools/shared/hostProcess already exposes HostProcessArguments (a Context.Reference defaulting to process.argv), which would add no requirement to configure while making the Windows/Linux cold-start path injectable and testable.

Both findings were posted as inline review comments.

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.

Reviewed scope

  • apps/desktop/src/app/DesktopDeepLink.ts (new service), DesktopDeepLink.test.ts
  • apps/desktop/src/app/DesktopApp.ts, apps/desktop/src/main.ts, apps/desktop/src/preload.ts
  • apps/desktop/src/electron/ElectronWindow.ts, apps/desktop/src/ipc/DesktopIpc.ts, apps/desktop/src/ipc/channels.ts
  • apps/web/src/routes/__root.tsx, packages/contracts/src/ipc.ts, updated desktop test harnesses

Checked and found compliant

  • Namespace subpath imports (effect/Effect, effect/Layer, ...); named import for @t3tools/contracts; local service modules imported as namespaces.
  • Canonical module shape: inline interface on Context.Service, real make, export const layer = Layer.effect(...); mirrors the existing DesktopClerk service.
  • Dependencies acquired from the environment (yield* ElectronApp.ElectronApp, ElectronWindow, DesktopIpc, DesktopEnvironment) and reflected in configure's requirement type, including Scope.Scope.
  • Effect.runPromiseWith is used only to bridge Electron OS-event callbacks, matching the established pattern in DesktopClerk.ts, DesktopWindow.ts, and DesktopIpc.ts; service instances are passed only in tests via provideService.
  • ElectronWindow.fromWebContentsId reuses the existing ElectronWindowOperationError with structured attributes plus cause, consistent with sibling operations; Effect.catch on IPC handler registration handles the entire (single-tag) error channel, which the conventions permit.
  • Tests cover new behavior rather than a mechanical refactor; harness updates to the new fromWebContentsId member are mechanical.

πŸ” Agent activity β€” 15 tool calls

πŸ’­ I'm reviewing the new DesktopDeepLink.ts file against our conventions, checking the import style and confirming that the service definition follows the expected order of imports, pure helpers, and then the main service logic. Looking closer, I notice a module-level mutable variable (earlyCapture) that holds state consumed later inside the service's configure logic β€” this looks like exactly the kind of hidden dependency our conventions warn against, since it's a module global rather than something passed through the environment. The justification is the macOS cold-start timing issue before the layer is constructed, but I still think this counts as a real violation worth flagging. Then there's runPromise used inside the service constructor's configure step. Normally that pattern would be flagged since runPromise belongs at application boundaries, not inside domain services β€” but here it's actually inside the Electron on("open-url", ...) callback, which is a legitimate native OS callback boundary, so this usage seems acceptable rather than a violation. I also want to check whether other desktop services in the repo handle Electron's native callbacks similarly, so I should look at how ElectronApp.on is typically wired up elsewhere. The mutable subscribers array in the closure seems like fine local state, not something that needs to be a Ref since it's not shared globally. The Effect.catch on the ipc.handle registration looks reasonable too β€” it's meant to intentionally handle the entire error channel, and logging error.message there seems like standard practice rather than a clear violation of the sanitized-logging rule. No new error definitions show up in this section either. For WebContentsId, I need to confirm the existing ElectronWindowOperationError class actually supports an "inspect-window" operation, and check whether wrapping a constructed error just to immediately orDie it fits the established pattern elsewhere in ElectronWindow.ts. I want to pull up the actual source for ElectronWindow.ts, ElectronApp.ts, and DesktopIpc.ts to verify these patterns directly rather than guessing. I'm also noting that DesktopIpc's optional event parameter, and the deep-link handler's event?.sender check with a comment about it being absent only in test-fabricated events, is a minor test-accommodation detail rather than anything the conventions explicitly call out.

1. browse_code β€” apps/desktop/src/electron/ElectronApp.ts

2. browse_code β€” apps/desktop/src/electron/ElectronWindow.ts

πŸ’­ The ElectronWindow addition looks consistent with existing patterns. Now I want to check if other desktop services use Effect.runPromiseWith inside listeners to see if there's precedent for that approach.

3. git_tools β€” runPromiseWith|runPromise|runFork

πŸ’­ This runPromise usage matches the existing convention for bridging native Electron callbacks, so it's not a finding. For the module-global earlyCapture, I want to check DesktopClerk.ts for a similar pre-layer capture pattern, like the OAuth deep-link early capture, to see if there's precedent.

4. browse_code β€” apps/desktop/src/app/DesktopClerk.ts

πŸ’­ I'm noting that DesktopDeepLink closely follows the pattern in DesktopClerk. Now I'm evaluating whether the module-level global for early-captured open URLs, populated at import time and drained later during configuration, counts as hiding a dependency rather than passing it explicitly. I'm also checking whether reading process.argv directly inside configure bypasses an existing host process abstraction, and whether the subscribers array kept in closure state (rather than a Ref, unlike pending) is a meaningful inconsistency or just normal service-local state similar to how mainWindowRef is handled elsewhere. The test file's use of fake service instances constructed via provideService and typed with ["Service"] looks fine and consistent with allowed test patterns, and the new tests are justified since they cover genuinely new behavior rather than a mechanical refactor. I'm also confirming that wrapping the ipc.handle registration errors with Effect.catch is appropriate since it fully handles that error channel by logging a bounded message, and I'm checking whether DesktopIpc.handle's Scope requirement is properly threaded through configure's own requirements. Everything about dependency acquisition checks out against the DesktopClerk pattern β€” services are pulled from the environment via yield* rather than hidden. That leaves the module-global early capture as the real concern: since it's mutable state that configure depends on implicitly, it's a hidden dependency and a testability hazard, since leftover state from one test could leak into another. I think this genuinely violates the "don't hide dependencies in module globals" rule. The right fix would be to have the capture function return a handle and provide it through the environment instead β€” as a layer or passed directly into make β€” rather than reading from an untracked global. I should check whether there's precedent elsewhere in the codebase for module-level let state in desktop main-process services, and also whether process.argv is read directly anywhere rather than through a proper host-process abstraction.

5. git_tools β€” ^let |^const [a-zA-Z]+: {|process.argv

6. git_tools β€” export

πŸ’­ There's a HostProcessArguments context reference for process.argv, so reading process.argv directly instead of yielding it from that service is a genuine hidden environment dependency, aligning with the principle that production services must acquire dependencies from the environment rather than module globals. I should check how HostProcessArguments is defined and used elsewhere to confirm.

7. browse_code β€” packages/shared/src/hostProcess.ts

8. git_tools β€” HostProcessArguments

πŸ’­ I notice there's already a Context.Reference for argv used server-side, but desktop's DesktopLifecycle.ts reads process.argv directly instead. Since a default value exists, switching to the Effect environment read would be trivially better and more testable, though it's not strictly a service dependency violation since process.argv is an ambient read rather than an injected service β€” existing desktop code already follows this same pattern. I'm also weighing how to interpret the ambiguous instruction about defaulting to failure versus outputting "All clear" β€” I need to reconcile that with the required JSON output format, so I'll treat "no findings" as a success state and reserve failure for actual clear violations. Let me weigh the strongest candidate findings before finalizing my decision. Looking at the module-level mutable global holding the Electron app source, this directly violates the convention against hiding dependencies in module globals or closures over singleton services β€” the configure function reads this hidden state via drainEarlyOpenUrls(), creating an undeclared input and fragile test-ordering dependency where a second capture call gets silently ignored. A convention-compliant fix would have main.ts pass the captured buffer explicitly as a value into the layer rather than relying on this hidden global. I'm weighing whether this is a hard failure or just worth flagging β€” the timing constraint genuinely does require pre-layer capture, so the global is arguably just the mechanism rather than a true hidden dependency, and reasonable reviewers might accept it with an explanatory comment. But since it does carry a singleton imperative API into service state, I lean toward treating it as a real violation worth failing on, even though it's debatable. I also want to check the optional event parameter on the DesktopIpc handler, though the conventions don't clearly address that case. Separately, I notice the subscriber logic mixes a Ref with a plain mutable array, and calls a side-effecting function directly inside an Effect.gen body rather than wrapping it in Effect.sync β€” a mild purity violation that isn't explicitly covered by the stated conventions either. I want to check if there's a repo-wide guideline file that addresses Ref usage or module-level globals to see if this strengthens the case.

9. browse_code β€” /{.macroscope/,AGENTS.md,CLAUDE.md,docs/dev/**}

10. browse_code β€” AGENTS.md

πŸ’­ I'll skip re-reading the check config since it's likely the same, and instead focus on double-checking DesktopDeepLink.ts for details like error handling and how ipc.handle interacts with Effect's catch semantics. Let me now look at DesktopIpc.ts as well.

11. browse_code β€” apps/desktop/src/ipc/DesktopIpc.ts

πŸ’­ Since DesktopIpcRegistrationError is the only tag in the error channel here, using Effect.catch to handle it entirely is consistent with the convention rather than needing catchTags. I want to see how other desktop services handle similar ipc.handle failures for comparison.

12. git_tools β€” ipc[.]handle|catchTags|Effect.catch

πŸ’­ I'm noticing there's already a DesktopLinuxUrlHandler.ts handling scheme registration and an ElectronProtocol, so I should check how they obtain the scheme from DesktopEnvironment to keep the new service consistent with existing patterns. Now weighing two possible findings: the module-level earlyCapture global being read as a hidden dependency inside configure, versus process.argv being read directly instead of going through the existing HostProcessArguments reference. The argv issue feels more concrete since there's already a Context.Reference precedent for it in the codebase, and using it would cost nothing while making the cold-start path testable β€” so I'm leaning toward flagging both together under one "hidden inputs to configure" comment. I'm checking whether these truly count as convention violations that should fail the check, since the rule requires verifying that runPromise/service-instance patterns are legitimate boundaries rather than fake dependency injection. The runPromise usage inside configure matches the established Electron native-callback pattern already used elsewhere in the repo, so that part looks legitimate β€” it's really the module global and argv access that remain the open question. Both findings share the same theme, so posting them together is reasonable. Let me also check whether the new IPC payload type should use Schema instead of a plain interface β€” looking at contracts conventions, other DesktopBridge payloads like MENU_ACTION_CHANNEL for comparison, though Schema usage seems out of scope for these particular Effect-service instructions. I also want to check whether the preload's manual validation instead of Schema decode falls under anything I'd normally flag, and whether there are any consolidated "effect" imports in the diff β€” there aren't. I'm also weighing the file layout order in DesktopDeepLink.ts against the canonical pattern of imports, error/schema declarations, the Context.Service tag, make, then layer β€” helpers appearing before the tag is a minor deviation similar to DesktopClerk, not worth flagging. Now I need exact line numbers in the diff for inline comments, since for a new file the whole thing is included and any line works. I'm locating the early-capture variable declaration and the process.argv reference in DesktopDeepLink.ts so I can pinpoint where to comment.

13. browse_code β€” apps/desktop/src/app/DesktopDeepLink.ts

14. browse_code β€” apps/desktop/src/app/DesktopDeepLink.ts

πŸ’­ I'll leave two inline comments: one flagging the hidden module-global dependency and suggesting explicit injection of the capture handle for testability, and another suggesting the process.argv usage be replaced with HostProcessArguments from the shared hostProcess module so it flows through the Effect environment instead. Since neither has an obvious direct patch (both cross cross-file or multi-hunk boundaries), I'll describe the fixes rather than provide diffs. Given the module-global comment is a genuine convention violation, I'll mark this review as a failure. I'll keep both comments concise, and note the correct line references for GitHub's line-comment API β€” lines 75-79 for the first, line 279 for the second. For the first comment, I'm explaining that the early-capture buffer is invisible module state consumed by configure, causing silent test pollution, and suggesting the capture handle be threaded through explicitly as a Context.Reference value instead. For the second, I'm noting that reading process.argv directly from the ambient global inside the service hides an environment dependency that should be acquired the same way. Now I'm confirming the review submission has the right body without needing an explicit event param, and double-checking that the docs file stays out of scope for this review.

15. modify_pr β€” Effect service conventions review: two findings in apps/desktop/src/app/DesktopDeepLink.ts, both about inputs to the service that never appear in the Effect environment. The service shape, make/layer naming, dependency acquisition via yield* Foo.Foo, and the native-callback runPromiseWith bridge all match the existing DesktopClerk/DesktopWindow patterns.


Agent Credits: 131 credits