Skip to content

M1: Recording Core + UI (Routing Off)#8684

Merged
Jasonnnz merged 6 commits into
feature/standalone-screen-recfrom
swarm/standalone-screen-rec/task-1
Feb 25, 2026
Merged

M1: Recording Core + UI (Routing Off)#8684
Jasonnnz merged 6 commits into
feature/standalone-screen-recfrom
swarm/standalone-screen-rec/task-1

Conversation

@Jasonnnz
Copy link
Copy Markdown
Contributor

@Jasonnnz Jasonnnz commented Feb 25, 2026

Summary

  • Add IPC contract types for recording lifecycle: RecordingOptions, RecordingStatus (client-to-server), RecordingStart and RecordingStop (server-to-client)
  • Create ScreenRecorder — app-agnostic screen recorder using ScreenCaptureKit + AVAssetWriter (H.264 video, optional AAC audio, 30fps)
  • Create RecordingManager — centralized orchestration with single-active recording guard, state machine, and IPC status reporting
  • Create source picker UI (RecordingSourcePickerView, RecordingSourcePickerViewModel, RecordingSourcePickerWindow) with display/window selection and audio toggle
  • Create recording HUD (RecordingHUDWindow) with animated red dot, elapsed timer, and stop button
  • Wire recording_start and recording_stop IPC callbacks through DaemonClientDaemonMessageRouterAppDelegate
  • Add permission checking with actionable guidance when screen recording access is denied
  • Add forceStop() on app termination to clean up active recordings

Routing is not wired in this milestone. Part of #8672, closes #8673.

Test plan

  • Verify TypeScript compiles: cd assistant && bunx tsc --noEmit (pre-existing errors only)
  • Verify IPC codegen produces valid Swift types
  • Verify Swift builds: cd clients/macos && ./build.sh
  • Verify IPCRecordingStart, IPCRecordingStop, IPCRecordingStatus, IPCRecordingOptions exist in generated Swift
  • Verify ServerMessage enum has recordingStart and recordingStop cases
  • Verify DaemonClient has onRecordingStart and onRecordingStop callbacks
  • Verify RecordingManager enforces single-active guard (returns false if already recording)
  • Verify source picker UI renders with design system tokens

🤖 Generated with Claude Code


Open with Devin

Co-Authored-By: Claude <noreply@anthropic.com>
@Jasonnnz Jasonnnz self-assigned this Feb 25, 2026
@Jasonnnz
Copy link
Copy Markdown
Contributor Author

@codex review

devin-ai-integration[bot]

This comment was marked as resolved.

chatgpt-codex-connector[bot]

This comment was marked as resolved.

@Jasonnnz
Copy link
Copy Markdown
Contributor Author

Addressed in #8686

Jasonnnz and others added 5 commits February 25, 2026 01:19
…uard, async HUD, picker close (#8686)

Co-authored-by: Vellum Assistant <assistant@vellum.ai>
Co-authored-by: Claude <noreply@anthropic.com>
#8688)

Co-authored-by: Vellum Assistant <assistant@vellum.ai>
Co-authored-by: Claude <noreply@anthropic.com>
…#8691)

Co-authored-by: Vellum Assistant <assistant@vellum.ai>
Co-authored-by: Claude <noreply@anthropic.com>
…d recording (#8692)

Co-authored-by: Vellum Assistant <assistant@vellum.ai>
Co-authored-by: Claude <noreply@anthropic.com>
…ng was a regression (#8694)

Co-authored-by: Vellum Assistant <assistant@vellum.ai>
Co-authored-by: Claude <noreply@anthropic.com>
@Jasonnnz Jasonnnz merged commit 853b581 into feature/standalone-screen-rec Feb 25, 2026
3 of 4 checks passed
@Jasonnnz Jasonnnz deleted the swarm/standalone-screen-rec/task-1 branch February 25, 2026 06:41
Copy link
Copy Markdown
Contributor

@devin-ai-integration devin-ai-integration Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 4 new potential issues.

View 13 additional findings in Devin Review.

Open in Devin Review

Comment on lines +122 to +123
captureWidth = Int(targetDisplay.width) * 2 // Retina
captureHeight = Int(targetDisplay.height) * 2
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Hardcoded 2x Retina multiplier produces incorrect capture resolution on non-Retina displays

The ScreenRecorder.start() method hardcodes * 2 when computing the capture dimensions from SCDisplay.width/height and SCWindow.frame, assuming all displays are 2x Retina.

Root Cause and Impact

Apple's documentation states that SCDisplay.width and SCDisplay.height return dimensions in points, not pixels. On a 2x Retina display (e.g., built-in MacBook Pro), points × 2 = pixels, so the hardcoded multiplier happens to be correct. However, on a non-Retina external monitor (common with 1080p or 1440p displays connected via HDMI/USB-C), the point dimensions equal the pixel dimensions (1x scale factor).

For a 1920×1080 non-Retina monitor:

  • targetDisplay.width = 1920 (points = pixels at 1x)
  • Config is set to 3840×2160 (double the actual pixel resolution)
  • ScreenCaptureKit delivers upscaled 3840×2160 frames
  • H.264 encoding runs at 4K resolution unnecessarily, wasting CPU/GPU and producing files ~4x larger than needed

The same issue affects window capture at ScreenRecorder.swift:105-106.

The correct approach is to query the backing scale factor for the target display (e.g., via NSScreen.backingScaleFactor) instead of assuming 2x.

Prompt for agents
In clients/macos/vellum-assistant/ComputerUse/ScreenRecorder.swift, replace the hardcoded `* 2` Retina multiplier on lines 105-106 and 122-123 with a dynamic scale factor lookup. For display capture (lines 122-123), find the matching NSScreen for the SCDisplay and use its backingScaleFactor. For window capture (lines 105-106), similarly resolve the appropriate screen. Example approach:

1. Add a helper function that finds the NSScreen matching an SCDisplay by comparing display IDs: `private static func scaleFactor(for display: SCDisplay) -> Int`
2. For display capture, use `Int(targetDisplay.width) * scaleFactor` instead of `Int(targetDisplay.width) * 2`
3. For window capture, determine which screen the window is on (or default to NSScreen.main) and use its backingScaleFactor
4. Fallback to 2 if no matching screen is found, to preserve current behavior on unknown configurations
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +66 to +70
guard !state.isActive else {
log.warning("Cannot start recording — already active (state=\(String(describing: self.state)), owner=\(self.ownerSessionId ?? "nil"))")
sendStatus(sessionId: sessionId, status: "failed", error: "Another recording is already active")
return false
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 sendStatus in single-active guard sends stale attachToConversationId from the existing recording

When RecordingManager.start() is called while another recording is already active, the "already active" guard at line 66 rejects the new request and calls sendStatus to report a failure. However, sendStatus (line 200) reads self.attachToConversationId, which at that point belongs to the currently active recording — not the new request that just failed.

Root Cause

The guard fires before self.attachToConversationId is updated (lines 72-73 only execute after the guard passes). So the recording_status message sent to the daemon for the new rejected session carries the old recording's attachToConversationId.

For example:

  1. Recording A starts with attachToConversationId = "conv-A"
  2. Recording B is requested with attachToConversationId = "conv-B"
  3. Guard fires at line 66, sendStatus is called at line 68
  4. sendStatus at line 200 reads self.attachToConversationId which is still "conv-A"
  5. Daemon receives a failure status for session B but with attachToConversationId = "conv-A"

Impact: The daemon receives an incorrect attachToConversationId on the failure status for the rejected recording request, which could cause incorrect state tracking or misattributed failures.

Suggested change
guard !state.isActive else {
log.warning("Cannot start recording — already active (state=\(String(describing: self.state)), owner=\(self.ownerSessionId ?? "nil"))")
sendStatus(sessionId: sessionId, status: "failed", error: "Another recording is already active")
return false
}
guard !state.isActive else {
log.warning("Cannot start recording — already active (state=\(String(describing: self.state)), owner=\(self.ownerSessionId ?? "nil"))")
sendStatus(sessionId: sessionId, status: "failed", error: "Another recording is already active", overrideConversationId: attachToConversationId)
return false
}
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +23 to +26
func show(onStart: @escaping (IPCRecordingOptions) -> Void, onCancel: @escaping () -> Void) {
// Dismiss any existing picker window
dismiss()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 RecordingSourcePickerWindow.dismiss() silently drops pending cancel callback

When RecordingSourcePickerWindow.show() is called while the picker is already open (e.g., a second recording_start arrives with promptForSource: true), the internal dismiss() at line 25 tears down the old window without firing its onCancelCallback. The daemon never receives a failure notification for the first recording request.

Root Cause

dismiss() at RecordingSourcePickerWindow.swift:73-78 sets window?.delegate = nil before closing, which prevents windowWillClose from firing. It also doesn't call fireCancel() itself. Then show() overwrites onCancelCallback at line 27 with the new callback, permanently losing the old one.

Sequence:

  1. recording_start feat: initialize Next.js app in /web directory #1 arrives with promptForSource: true → picker shown, onCancelCallback set to notify daemon about request feat: initialize Next.js app in /web directory #1
  2. recording_start feat: add platform terraform for GKE deployment #2 arrives with promptForSource: trueshow() calls dismiss() (no cancel fired), then sets onCancelCallback to notify daemon about request feat: add platform terraform for GKE deployment #2
  3. The daemon never learns that request feat: initialize Next.js app in /web directory #1 was abandoned

Impact: The daemon would continue waiting for a status update on the first recording request indefinitely. This is an edge case requiring two rapid recording_start messages with promptForSource: true.

Suggested change
func show(onStart: @escaping (IPCRecordingOptions) -> Void, onCancel: @escaping () -> Void) {
// Dismiss any existing picker window
dismiss()
func show(onStart: @escaping (IPCRecordingOptions) -> Void, onCancel: @escaping () -> Void) {
// Fire any pending cancel callback before tearing down the old window
fireCancel()
// Dismiss any existing picker window
dismiss()
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +193 to +197
do {
try await captureStream.startCapture()
} catch {
throw RecorderError.streamStartFailed(error.localizedDescription)
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 ScreenRecorder.start() leaks resources when startCapture() fails — no cleanup of writer, stream, or delegate

When SCStream.startCapture() throws in ScreenRecorder.start(), the method re-throws without calling cleanUpWriter(). This leaves stale references in assetWriter, videoInput, audioInput, stream, and outputDelegate — none of which are cleaned up.

Root Cause

At ScreenRecorder.swift:190-196, all instance state is set before attempting to start the capture:

self.stream = captureStream       // line 190
do {
    try await captureStream.startCapture()  // line 194 — can throw
} catch {
    throw RecorderError.streamStartFailed(error.localizedDescription)  // line 196 — leaks!
}

By this point, self.assetWriter (line 177), self.videoInput (line 161), self.audioInput (line 174), self.outputDelegate (line 181), and self.stream (line 190) are all populated. The thrown error propagates to RecordingManager.start() which catches it, but the ScreenRecorder's internal state remains stale.

Because isRecordingActive is never set to true (that happens at line 199), both stop() and cancelRecording() will early-return on their isRecordingActive guard, leaving these resources stranded until the next successful start() overwrites them.

Impact: Leaked AVAssetWriter, SCStream, and delegate objects persist in memory. The StreamOutputDelegate holds a strong reference back to ScreenRecorder (ScreenRecorder.swift:332), creating a retain cycle that prevents deallocation if the recorder is ever released. In practice the recorder lives as long as the app, so the impact is limited to inconsistent internal state and minor memory waste.

Suggested change
do {
try await captureStream.startCapture()
} catch {
throw RecorderError.streamStartFailed(error.localizedDescription)
}
do {
try await captureStream.startCapture()
} catch {
cleanUpWriter()
throw RecorderError.streamStartFailed(error.localizedDescription)
}
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Jasonnnz added a commit that referenced this pull request Feb 25, 2026
* feat: add standalone screen recording core, UI, and IPC types

Co-Authored-By: Claude <noreply@anthropic.com>

* fix: address M1 review feedback — synchronous forceStop, start race guard, async HUD, picker close (#8686)

Co-authored-by: Vellum Assistant <assistant@vellum.ai>
Co-authored-by: Claude <noreply@anthropic.com>

* fix: stop stale recorder on start guard, fix forceStop status ordering (#8688)

Co-authored-by: Vellum Assistant <assistant@vellum.ai>
Co-authored-by: Claude <noreply@anthropic.com>

* fix: gate stale-start recorder cancel to avoid cross-session teardown (#8691)

Co-authored-by: Vellum Assistant <assistant@vellum.ai>
Co-authored-by: Claude <noreply@anthropic.com>

* fix: only skip stale recorder cancel when another session is confirmed recording (#8692)

Co-authored-by: Vellum Assistant <assistant@vellum.ai>
Co-authored-by: Claude <noreply@anthropic.com>

* fix: revert stale-start cancel to !state.isActive — state != .recording was a regression (#8694)

Co-authored-by: Vellum Assistant <assistant@vellum.ai>
Co-authored-by: Claude <noreply@anthropic.com>

---------

Co-authored-by: Vellum Assistant <assistant@vellum.ai>
Co-authored-by: Claude <noreply@anthropic.com>
Jasonnnz added a commit that referenced this pull request Feb 25, 2026
* feat: add standalone screen recording core, UI, and IPC types

Co-Authored-By: Claude <noreply@anthropic.com>

* fix: address M1 review feedback — synchronous forceStop, start race guard, async HUD, picker close (#8686)

Co-authored-by: Vellum Assistant <assistant@vellum.ai>
Co-authored-by: Claude <noreply@anthropic.com>

* fix: stop stale recorder on start guard, fix forceStop status ordering (#8688)

Co-authored-by: Vellum Assistant <assistant@vellum.ai>
Co-authored-by: Claude <noreply@anthropic.com>

* fix: gate stale-start recorder cancel to avoid cross-session teardown (#8691)

Co-authored-by: Vellum Assistant <assistant@vellum.ai>
Co-authored-by: Claude <noreply@anthropic.com>

* fix: only skip stale recorder cancel when another session is confirmed recording (#8692)

Co-authored-by: Vellum Assistant <assistant@vellum.ai>
Co-authored-by: Claude <noreply@anthropic.com>

* fix: revert stale-start cancel to !state.isActive — state != .recording was a regression (#8694)

Co-authored-by: Vellum Assistant <assistant@vellum.ai>
Co-authored-by: Claude <noreply@anthropic.com>

---------

Co-authored-by: Vellum Assistant <assistant@vellum.ai>
Co-authored-by: Claude <noreply@anthropic.com>
Jasonnnz added a commit that referenced this pull request Feb 25, 2026
* feat: add standalone screen recording core, UI, and IPC types

Co-Authored-By: Claude <noreply@anthropic.com>

* fix: address M1 review feedback — synchronous forceStop, start race guard, async HUD, picker close (#8686)

Co-authored-by: Vellum Assistant <assistant@vellum.ai>
Co-authored-by: Claude <noreply@anthropic.com>

* fix: stop stale recorder on start guard, fix forceStop status ordering (#8688)

Co-authored-by: Vellum Assistant <assistant@vellum.ai>
Co-authored-by: Claude <noreply@anthropic.com>

* fix: gate stale-start recorder cancel to avoid cross-session teardown (#8691)

Co-authored-by: Vellum Assistant <assistant@vellum.ai>
Co-authored-by: Claude <noreply@anthropic.com>

* fix: only skip stale recorder cancel when another session is confirmed recording (#8692)

Co-authored-by: Vellum Assistant <assistant@vellum.ai>
Co-authored-by: Claude <noreply@anthropic.com>

* fix: revert stale-start cancel to !state.isActive — state != .recording was a regression (#8694)

Co-authored-by: Vellum Assistant <assistant@vellum.ai>
Co-authored-by: Claude <noreply@anthropic.com>

---------

Co-authored-by: Vellum Assistant <assistant@vellum.ai>
Co-authored-by: Claude <noreply@anthropic.com>
Jasonnnz added a commit that referenced this pull request Feb 25, 2026
* M1: Recording Core + UI (Routing Off) (#8684)

* feat: add standalone screen recording core, UI, and IPC types

Co-Authored-By: Claude <noreply@anthropic.com>

* fix: address M1 review feedback — synchronous forceStop, start race guard, async HUD, picker close (#8686)

Co-authored-by: Vellum Assistant <assistant@vellum.ai>
Co-authored-by: Claude <noreply@anthropic.com>

* fix: stop stale recorder on start guard, fix forceStop status ordering (#8688)

Co-authored-by: Vellum Assistant <assistant@vellum.ai>
Co-authored-by: Claude <noreply@anthropic.com>

* fix: gate stale-start recorder cancel to avoid cross-session teardown (#8691)

Co-authored-by: Vellum Assistant <assistant@vellum.ai>
Co-authored-by: Claude <noreply@anthropic.com>

* fix: only skip stale recorder cancel when another session is confirmed recording (#8692)

Co-authored-by: Vellum Assistant <assistant@vellum.ai>
Co-authored-by: Claude <noreply@anthropic.com>

* fix: revert stale-start cancel to !state.isActive — state != .recording was a regression (#8694)

Co-authored-by: Vellum Assistant <assistant@vellum.ai>
Co-authored-by: Claude <noreply@anthropic.com>

---------

Co-authored-by: Vellum Assistant <assistant@vellum.ai>
Co-authored-by: Claude <noreply@anthropic.com>

* M2: Daemon Recording Control + Deterministic Mapping (#8696)

* feat: add daemon recording start/stop handlers with deterministic mapping

Co-Authored-By: Claude <noreply@anthropic.com>

* fix: prevent map leaks in recording handlers — duplicate start + missing socket (#8697)

Co-authored-by: Vellum Assistant <assistant@vellum.ai>
Co-authored-by: Claude <noreply@anthropic.com>

---------

Co-authored-by: Vellum Assistant <assistant@vellum.ai>
Co-authored-by: Claude <noreply@anthropic.com>

* M3: Intent Routing + Strict Classification (Enable Flag) (#8699)

* feat: add recording intent detection and standalone routing with feature flag

Co-Authored-By: Claude <noreply@anthropic.com>

* fix: address M3 PR #8699 review feedback (#8700)

* fix: add isStopRecordingOnly guard, sessionId in message_complete, check stop return value

Co-Authored-By: Claude <noreply@anthropic.com>

* fix: handle polite filler words in isStopRecordingOnly and isRecordingOnly (#8701)

Co-authored-by: Vellum Assistant <assistant@vellum.ai>
Co-authored-by: Claude <noreply@anthropic.com>

---------

Co-authored-by: Vellum Assistant <assistant@vellum.ai>
Co-authored-by: Claude <noreply@anthropic.com>

---------

Co-authored-by: Vellum Assistant <assistant@vellum.ai>
Co-authored-by: Claude <noreply@anthropic.com>

* M4: Finalization + Attachment Delivery (#8703)

* feat: add recording finalization and attachment delivery for standalone recordings

Co-Authored-By: Claude <noreply@anthropic.com>

* fix: address M4 PR #8703 review feedback (#8709)

* fix: use file-backed attachment storage, include attachment in message_complete, notify on failure

Co-Authored-By: Claude <noreply@anthropic.com>

* fix: make file-backed attachments retrievable (#8713)

* fix: add file-backed attachment retrieval for recordings

Co-Authored-By: Claude <noreply@anthropic.com>

* fix: validate Range header bounds and handle suffix ranges (#8716)

Co-authored-by: Vellum Assistant <assistant@vellum.ai>
Co-authored-by: Claude <noreply@anthropic.com>

---------

Co-authored-by: Vellum Assistant <assistant@vellum.ai>
Co-authored-by: Claude <noreply@anthropic.com>

---------

Co-authored-by: Vellum Assistant <assistant@vellum.ai>
Co-authored-by: Claude <noreply@anthropic.com>

---------

Co-authored-by: Vellum Assistant <assistant@vellum.ai>
Co-authored-by: Claude <noreply@anthropic.com>

* M5: Bundled Recording Skill (Activation Rules Only) (#8717)

* feat: add bundled screen-recording skill with activation rules

Co-Authored-By: Claude <noreply@anthropic.com>

* fix: add macOS OS gate to screen-recording skill (#8718)

* fix: restrict screen-recording skill to macOS via os metadata

Co-Authored-By: Claude <noreply@anthropic.com>

* fix: move os gate inside vellum metadata namespace (#8722)

Co-authored-by: Vellum Assistant <assistant@vellum.ai>
Co-authored-by: Claude <noreply@anthropic.com>

---------

Co-authored-by: Vellum Assistant <assistant@vellum.ai>
Co-authored-by: Claude <noreply@anthropic.com>

---------

Co-authored-by: Vellum Assistant <assistant@vellum.ai>
Co-authored-by: Claude <noreply@anthropic.com>

* M6: Tests + Docs for standalone screen recording (#8727)

* feat: add recording intent and handler tests

Add comprehensive tests for recording-intent.ts (all 6 exported
functions: detectRecordingIntent, isRecordingOnly, detectStopRecordingIntent,
stripRecordingIntent, stripStopRecordingIntent, isStopRecordingOnly) and
recording handler (start/stop/status flows with proper mocking).

Co-Authored-By: Claude <noreply@anthropic.com>

* docs: add standalone screen recording section to ARCHITECTURE.md

Document the standalone recording lifecycle, key files, IPC messages,
intent routing, file-backed attachments, and include a Mermaid sequence
diagram showing the recording flow.

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: Vellum Assistant <assistant@vellum.ai>
Co-authored-by: Claude <noreply@anthropic.com>

* fix: prevent recording ownership overwrite and ensure sessionId on stop (#8828)

Co-authored-by: Vellum Assistant <assistant@vellum.ai>
Co-authored-by: Claude <noreply@anthropic.com>

* fix: move duplicate-start messaging to callers, return null from handleRecordingStart (#8829)

Co-authored-by: Vellum Assistant <assistant@vellum.ai>
Co-authored-by: Claude <noreply@anthropic.com>

* fix: type narrowing for nullable handleRecordingStart + add task_routed to else branch (#8830)

Co-authored-by: Vellum Assistant <assistant@vellum.ai>
Co-authored-by: Claude <noreply@anthropic.com>

* fix: add task_routed to stop-recording path in handleTaskSubmit (#8831)

Co-authored-by: Vellum Assistant <assistant@vellum.ai>
Co-authored-by: Claude <noreply@anthropic.com>

* fix: add global single-active recording guard and sessionId to stop-recording text delta (#8834)

Co-authored-by: Vellum Assistant <assistant@vellum.ai>
Co-authored-by: Claude <noreply@anthropic.com>

* fix: resolve handleRecordingStop globally for cross-conversation stop (#8836)

Co-authored-by: Vellum Assistant <assistant@vellum.ai>
Co-authored-by: Claude <noreply@anthropic.com>

* fix: always create new assistant message for recordings + notify on missing file (#8837)

Co-authored-by: Vellum Assistant <assistant@vellum.ai>
Co-authored-by: Claude <noreply@anthropic.com>

* refactor: move Recording files and use raw content endpoint

- Move Recording*.swift and ScreenRecorder.swift from ComputerUse/ to Recording/
- Switch InlineVideoAttachmentView to fetch raw bytes from /content endpoint
  instead of base64-decoding JSON, removing intermediate decode step
- Stop excluding own app windows from screen capture filter

Co-Authored-By: Claude <noreply@anthropic.com>

* fix: fetch thumbnail for file-backed attachments

- Extract thumbnail from content endpoint for lazy-load attachments
  that have no inline base64 data

Co-Authored-By: Claude <noreply@anthropic.com>

* Add microphone capture support to screen recording

- Add includeMicrophone option to IPC RecordingOptions contract
- Regenerate IPCContractGenerated.swift with new field
- Add microphone toggle to RecordingSourcePickerView
- Pass includeMicrophone through RecordingManager to ScreenRecorder
- Add separate AAC mic track in AVAssetWriter (mono, 64kbps)
- Register SCStream microphone output and handle in sample buffer
- Check/request microphone permission before recording starts
- Gate microphone APIs behind macOS 15 availability checks

Co-Authored-By: Claude <noreply@anthropic.com>

* fix: address PR review feedback for recording stop edge cases

1. misc.ts: Always call handleRecordingStop even when socket has no prior
   session binding. Previously, "stop recording" from task_submit with no
   prior session would skip the stop attempt entirely.

2. recording.ts: Don't clean up deterministic maps when no socket is found.
   Cleaning up orphaned the client-side recording (still running) while the
   daemon thought no recording was active, blocking future starts.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: harden recording security and cleanup on disconnect

1. Require tracked recording IDs in recording_status — reject unknown
   session IDs to prevent forged status messages from attaching arbitrary
   files via the finalization path.

2. Restrict accepted file paths to the recordings directory to prevent
   path traversal attacks in crafted IPC messages.

3. Clean up recording state on socket disconnect to prevent stale entries
   from blocking future recordings when a client crashes or disconnects.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: clean up recording maps on path rejection and support daemon restart

- Extract cleanupMaps() helper to ensure map cleanup runs on all exit
  paths, including when path validation rejects a file outside the
  allowed recordings directory (previously skipped by break)
- Fall back to attachToConversationId when in-memory maps are missing
  after daemon restart, preserving recording finalization across restarts
- Remove dead else-if branch that was unreachable after early return

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: clean recording state by socket, not session ID, and disable mic on denied

- Replace cleanupRecordingForSession(sessionId) with cleanupRecordingsOnDisconnect()
  which iterates all recording entries instead of a single session ID. Fixes the case
  where a recording started in conversation A is missed when the socket disconnects
  while bound to conversation B.
- Move cleanup call outside the sessionId check so it runs regardless of session state.
- When microphone permission is denied, rebuild options with includeMicrophone=false
  so recording starts without mic instead of potentially failing.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: notify client when recording path fails security validation

When a recording_status arrives with a file path outside the allowed
recordings directory, send a user-visible error message before cleaning
up state, so the user isn't left without feedback.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: scope recording cleanup to disconnecting socket, not global

cleanupRecordingsOnDisconnect now accepts the disconnecting socket and a
conversation-to-socket lookup so it only clears recordings owned by
conversations bound to that socket. This prevents an unrelated socket
disconnect from dropping another session's active recording state.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: notify client when recording stops without producing a file

When recording_status arrives with status 'stopped' but no filePath,
send a user-visible message explaining the recording produced no file
instead of silently cleaning up.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: route recording notifications via reporting socket

Use the socket that delivered recording_status as the primary recipient
for completion/failure notifications. This ensures notifications reach
the client even when the user has switched to a different conversation
since starting the recording.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* chore: regenerate ipc-contract-inventory.json after rebase

Add recording_status, recording_start, and recording_stop wire types
to the inventory after rebasing onto latest main.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: use file_path column for file-backed attachment detection

Use getFilePathForAttachment() instead of !dataBase64 to distinguish
file-backed attachments from valid zero-byte inline uploads. The
file_path column is the authoritative indicator of file-backed storage.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: add stop-acknowledgement timeout to prevent stale recording state (#9024)

Co-authored-by: Vellum Assistant <assistant@vellum.ai>
Co-authored-by: Claude <noreply@anthropic.com>

* fix: delete files only for confirmed orphans and suppress non-owner stop status (#9040)

Co-authored-by: Vellum Assistant <assistant@vellum.ai>
Co-authored-by: Claude <noreply@anthropic.com>

* fix: skip full video download for lazy attachment thumbnails (#9045)

Co-authored-by: Vellum Assistant <assistant@vellum.ai>
Co-authored-by: Claude <noreply@anthropic.com>

* fix: replace dead-branch MIME type ternary with lookup map (#9049)

* fix: replace dead-branch MIME type ternary with lookup map

Co-Authored-By: Claude <noreply@anthropic.com>

* fix: use Map for MIME types to avoid prototype pollution

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Vellum Assistant <assistant@vellum.ai>
Co-authored-by: Claude <noreply@anthropic.com>

* fix: resolve symlinks in recording file path validation (#9046)

* fix: resolve symlinks in recording file path validation

Co-Authored-By: Claude <noreply@anthropic.com>

* fix: also resolve allowedDir via realpathSync for APFS firmlink compatibility

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Vellum Assistant <assistant@vellum.ai>
Co-authored-by: Claude <noreply@anthropic.com>

* fix: delete orphaned conversation when recording start is rejected (#9048)

* fix: delete orphaned conversation when recording start is rejected

Co-Authored-By: Claude <noreply@anthropic.com>

* fix: keep conversation on rejection, only unbind socket to avoid FK violations

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Vellum Assistant <assistant@vellum.ai>
Co-authored-by: Claude <noreply@anthropic.com>

---------

Co-authored-by: Vellum Assistant <assistant@vellum.ai>
Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant