Skip to content

fix(preview): guard recording startup races

MacroscopeApp / Macroscope - Effect Service Conventions failed Aug 31, 2026 in 2m 35s

Effect Service Conventions: 1 issue found

1 convention violation found.

  • apps/web/src/browser/browserRecording.ts (lines 444-448): the newly added mime-type resolution signals a pure domain condition by throwing a bare Error ("MediaRecorder did not report its output format.") inside the try that wraps everything into BrowserRecordingOperationError({ operation: "save-artifact" }). The failure reason survives only as a manufactured cause; it should be a Schema.TaggedErrorClass with structured attributes and a message derived from them, raised outside the save-artifact wrapper.

An inline comment with a suggested fix has been posted.

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/ipc/methods/preview.ts
  • apps/desktop/src/preview/Manager.ts
  • apps/desktop/src/preview/Manager.test.ts
  • apps/web/src/browser/browserRecording.ts
  • apps/web/src/browser/browserRecording.test.ts
  • packages/contracts/src/ipc.ts

Conforming aspects verified

  • PreviewRecordingSourceSizeUnavailableError is defined with Schema.TaggedErrorClass, carries structured tabId/webContentsId attributes, derives message from them, correctly omits a cause (pure validation failure), and is registered in the PreviewManagerError union. This resolves the issue flagged on the prior run at commit 7c0541f.
  • startRecording in apps/desktop/src/ipc/methods/preview.ts was converted from the void-only tabMethod helper to DesktopIpc.makeIpcMethod with an explicit result schema; imports remain subpath namespaces (* as Effect, * as Schema, * as PreviewManager) and the service is acquired from the environment via yield* PreviewManager.PreviewManager inside the handler.
  • The updated PreviewManager interface signature (startRecording: (tabId: string) => Effect.Effect<DesktopPreviewRecordingSource, PreviewManagerError>) stays inline in the Context.Service declaration; no shape type was introduced.
  • DesktopPreviewRecordingSource / DesktopPreviewRecordingSourceSchema in packages/contracts/src/ipc.ts match the surrounding interface + Schema.Codec declaration style, and the bridge contract plus apps/desktop/src/preload.ts propagate the new return value.
  • No new ManagedRuntime.make, runPromise, runPromiseExit, Layer.succeed, service-instance parameters, catchTag, or predicate-based catchIf were introduced in the touched code. subscribeRecordingFrames is still driven by the debugger screencast path in Manager.ts, so it is not dead API surface.
  • Behavior changes on both sides are covered by updated focused tests rather than new mechanical test scaffolding.

🔍 Agent activity — 13 tool calls

💭 Checking whether the previously flagged issue about throwing a bare Error inside attempt was fixed — it now uses the proper PreviewRecordingSourceSizeUnavailableError tagged error, so that's resolved. Now I'm going through the current diff for new violations, starting with the contracts schema file which looks fine. Looking at the browser recording error operation discriminator, I notice "subscribe-frames" and "wait-first-frame" were removed while "capture-media-stream" and "cleanup" were added — I want to verify whether "stop-media-recorder" is still declared but now unused, so I should pull up the full file to check.

1. browse_code — apps/web/src/browser/browserRecording.ts

2. browse_code — apps/desktop/src/preview/Manager.ts

💭 I'm checking the cleanup error handling in browserRecording.ts, noting they replaced the "stop-media-recorder" op error with a "cleanup" op error and merged multiple failures into an AggregateError. I want to grep whether "stop-media-recorder" is still referenced elsewhere, and I'm considering whether combining two distinct failures under one "cleanup" label violates the convention against encoding the same distinction twice.

3. browse_code — apps/web/src/browser/browserRecording.ts

💭 Let me check if the "stop-media-recorder" literal is still referenced anywhere else.

4. git_tools — stop-media-recorder

5. browse_code — apps/web/src/browser/browserRecording.ts

💭 This file is a promise-based renderer rather than an Effect service, using TaggedErrorClass with an operation discriminator. I'm checking the convention about exporting schema predicates directly — noticing that isBrowserRecordingOperationError on line 59 is private but not exported, and looking at whether isStartupWaitTimeout follows the same pattern. That predicate seems to have existed before this PR, so it's pre-existing and out of scope. I'm shifting to review the desktop Manager.ts changes: startRecording returns a DesktopPreviewRecordingSource and wraps failures with the pre-existing PreviewOperationError pattern, and PreviewRecordingSourceSizeUnavailableError looks properly structured. I want to check why startRecording's IPC method switched from the tabMethod helper to an inline DesktopIpc.makeIpcMethod call — likely because it now needs a result schema — along with Effect.fn naming and import style in preview.ts, plus the nullable scope: Scope.Closeable | null on FrameCaptureSession, which isn't really an Effect service convention issue but adds complexity. Let me pull up preview.ts to look closer.

6. browse_code — apps/desktop/src/ipc/methods/preview.ts

💭 I'm checking whether subscribeRecordingFrames and the PREVIEW_RECORDING_FRAME_CHANNEL forwarding are now dead code since recording no longer emits frames directly, though the conventions don't explicitly cover cleaning up dead code paths like this.

7. git_tools — recordingFrameListenersRef|recording-frame|subscribeRecordingFrames

💭 I'm noticing line 1114 in Manager.ts still delivers recording frames, possibly through the debugger screencast path, so I want to check lines 1080–1140 to confirm.

8. browse_code — apps/desktop/src/preview/Manager.ts

💭 So the recording frames path is still active, confirming subscribeRecordingFrames isn't dead code. Now I'm turning to review the changed regions in Manager.ts more carefully—the startFrameCapture logic, the closeable scope change, the captureInitialFrame flag, and pictureInPictureFrame handling—though those are logic concerns rather than convention issues, which isn't what this check is meant to catch. Instead I need to focus on Effect service conventions: imports, service definitions, dependency acquisition, error/predicate patterns, layout, and change discipline. Looking at the error handling, the generic PreviewOperationError pattern with new operation strings for measuring source and getting media source ID follows existing conventions fine, and the new PreviewRecordingSourceSizeUnavailableError is a clean validation error properly added to the PreviewManagerError union. On the web side, I'm checking whether reusing the "cleanup" operation string for two different error aggregations (standalone media cleanup vs. a stop+cleanup composite) violates the rule against encoding the same distinction twice—but it doesn't seem to apply here since it's not a single-value operation case. I'm also considering whether an Error is being manufactured just to populate some field unnecessarily. I've spotted a genuine violation: in finalizeBrowserRecording, newly introduced code throws a bare Error stating the MediaRecorder didn't report its output format, inside a try block mapped to a "save-artifact" operation error—this manufactures an Error purely to populate a cause, matching a pattern flagged previously on the desktop side but now appearing fresh here since the mimeType resolution logic changed from a null check to this thrown error. Since it's a different file and line than the earlier flagged instance, it's not a duplicate finding, so I'm weighing whether it's worth raising given the explicit convention against creating errors without an underlying failure. I'm also checking recordingStartupCancelledError and its default cause parameter that similarly manufactures an Error, along with the AggregateError usage wrapping errors[0] as cause—but both appear to be pre-existing code untouched by this diff, so I'll skip flagging those. Similarly, the error-creation helper pattern in recordingStartupCancelledError matches another flagged convention violation, but again it predates this change. Now I want to check the contracts file to see if DesktopPreviewRecordingSource and sibling interfaces like DesktopPreviewRecordingFrame consistently use readonly properties, so I'm looking at the surrounding schema definitions.

9. browse_code — packages/contracts/src/ipc.ts

💭 The desktop-side startRecording interface change looks fine, and the null hostWebContents mapping to PreviewMainWindowClosedError is semantically a bit odd but not a convention issue. Now I should check the web-side test mocks, though those aren't in the service layer. The tests were rewritten because behavior changed, which fits the discipline requirement that backend changes need focused test updates — that's satisfied. The mimetype and unused-import differences aren't convention concerns either. I want to verify the startRecording contract change actually propagated through the bridge preload layer, so let me look at the preload implementation to confirm it returns the right value.

10. git_tools — startScreencast

11. browse_code — apps/desktop/src/preload.ts

💭 I'm flagging the bare Error thrown in finalizeBrowserRecording when MediaRecorder doesn't report its output format—this looks like it violates the convention against manufacturing an Error just to populate a cause, since it should instead be a proper validation failure using Schema.TaggedErrorClass with structured attributes. I'm sketching a fix: introduce a tagged error class like BrowserRecordingFormatUnavailableError carrying the tabId, with a message derived from that attribute. Since the current throw sits inside the try block that wraps into save-artifact, I need to restructure so the mimeType resolution happens before the try and the tagged error is thrown there directly rather than getting caught and re-wrapped. Now I'm looking at another spot — the composite stop-and-cleanup path around line 508, where a result.error that's already a structured BrowserRecordingOperationError gets wrapped again into a new cleanup error's AggregateError cause. The convention here is that at a translation boundary, an already-structured domain error belonging to the declared error channel should pass through unwrapped, and only unknown or lower-level failures should get wrapped. I want to check what the merge-base version of that block did to see if this double-wrapping is new behavior or pre-existing.

12. browse_code — apps/web/src/browser/browserRecording.ts

💭 I confirm "cleanup" already existed at the merge base, so that's not a new change. The remaining real finding is the bare Error thrown for a missing mimeType, and I'm checking whether the test file's added expectations reveal anything else worth flagging. I check the exported recordingFileExtension helper, noting it's exported purely for testing purposes, which is fine convention-wise even though its mapping of video/x-matroska to "matroska" is a bit odd but not a violation. I also glance at startFrameCapture's boolean naming, which seems fine. Deciding to file one inline comment on the bare Error, I trace the exact line number in the diff where the throw statement was added, confirming it falls within the added hunk. I think through how to phrase the comment—describing the issue and suggesting a fix, though a clean inline diff suggestion isn't feasible since it would require introducing a new error class and reordering logic, so I'll need to describe the fix in prose rather than a diff. I realize the throw happens inside a try block, meaning it gets caught and re-wrapped as a generic operation error anyway, so the real fix requires moving the mimeType resolution outside the try — I'll explain that in the comment. Given this mirrors a previously flagged pattern, I decide it's worth posting and marking the check as failing. After pinning down the precise line numbers to comment on, I confirm there's no need to reply to the earlier resolved thread on the desktop side, then move to drafting the actual comment text.

13. modify_pr — One convention issue found in the changed error-handling code. Everything else (the new PreviewRecordingSourceSizeUnavailableError tagged error, its union registration, the DesktopIpc.makeIpcMethod conversion for startRecording, and the contracts schema) follows the service conventions.


Agent Credits: 115 credits

Token usage: 24 uncached input · 740,178 cache-read input · 79,868 cache-write input · 8,970 output
Agent Credits may also include non-token charges from external tools such as web research.