Skip to content

cmux mcp: video_frames and video_info, so an agent looks into a video without ffmpeg - #30921

Merged
teamleaderleo merged 6 commits into
feat-cmux-nextfrom
lane/agent-video-frames
Nov 1, 2026
Merged

teamleaderleo merged 6 commits into
feat-cmux-nextfrom
lane/agent-video-frames

Conversation

@teamleaderleo

@teamleaderleo teamleaderleo commented Nov 1, 2026 •

Copy link
Copy Markdown
Collaborator

Leo, 2026-11-01, after he attached a 16 s mp4 in a cmux NEXT chat and the agent had to hand-write ffmpeg -vf fps=1,scale=640:-1,tile=4x4 into /tmp to look at it: "i feel like cmux-next should give u better tools to deal with this kind of thing ... you can see what we can work on", then "can you help work on all these".

What

Two new tools on the cmux MCP server (cmux-tui cli/mcp/video_tools.rs), listed in cmux_exec's common tools so agents pick them over ffmpeg:

  • video_info {path}: duration (ms), width and height as shown (after rotation), fps, has audio.
  • video_frames {path, at | range | scenes, count, maxSide}: frames as MCP image blocks, plus one text block with the duration, size, fps, audio and each frame's time.
    • at: [seconds…]: exact frames at those times (1 to 16).
    • range: {from, to, count}: count frames evenly from from to to, both included (defaults 0, the end, 8).
    • scenes: true (with count, default 8): the first frame plus the moments the picture changes most, in time order, each with its change score.
    • neither: count frames at the middles of equal parts of the whole video (default 8).
    • maxSide: longest side in px, 64 to 2048, default 1280; never upscaled.

Bounded output: at most 16 frames a call and 6 MiB of base64 JPEG. A call over the budget returns the first frames and says how many it left out, and what to ask for next. cmux_exec's per-script image cap goes from 8 to 16 (video_tools::MAX_FRAMES) so a 16-frame call fits; its 8 MiB byte cap is unchanged.

How

  • Rust (the logic): validates arguments, resolves the path (absolute, ~/, or relative to the chat's folder, as open does), picks the times and the decode tolerance (exact for at, a quarter of the spacing otherwise), finds scene changes from 64 small grey samples (mean absolute difference between neighbours, a 1.5-grey-level noise floor), and builds the image blocks. The result has no structuredContent, so cmux_exec moves the images into its own result (code_mode::script_answer).
  • Swift (the decoder): an app control method video.frames (CmuxNextApp/Control/VideoControl.swift) over a new AgentVideoFrames (CmuxNextAgentPane/AgentVideoFrames.swift). It uses the composer's AVFoundation decoder and its helpers from AgentPaneVideoInfo (isRegularFile, jpeg, and forbidAll reference restrictions). It decodes only the asked frames, never the whole file.
  • Same file, same machine: the server sends the file's device and inode as it sees them. The app refuses with not_found if its own stat of the path differs, so a chat whose MCP server runs on another machine never gets frames from a same-named file on the Mac.

Remote and Cloud sessions

The decode runs in the cmux app on the Mac. In a Cloud or ssh chat the MCP server runs on the remote machine, and the call fails cleanly in one of two ways. If no app answers, the error is app.not_found or app.unreachable. If the path belongs to another machine, the app answers not_found and the message tells the agent to use that machine's own tools. Follow-up: a decoder on the remote side (an ffmpeg/ffprobe backend in the same Rust module, picked when no local app owns the file) gives remote chats the same tool with the same schema.

Overlap

Another lane is changing AgentPaneVideoInfo.swift's frame picking (scene detection, timestamps). This PR does not touch that file. It only calls its existing isRegularFile and jpeg helpers from a new file. Scene picking for this tool lives in Rust (video_tools::scene_times), since base has no Swift scene picker. If the other lane's Swift picker lands, the tool can keep its own or switch to it later; nothing conflicts.

Tests

  • Rust, video_tools::tests: schema and caps, argument errors, time picking and clamping, scene picking, image blocks after one text block, the probe-then-frames request shape (path, dev/ino, side, tolerance, budget), the scene two-pass flow, left-out frames, a refusal from the app, and a missing file.
  • Swift, AgentVideoFramesTests: numbers only, frames at exact times and size (red then blue, never upscaled), grey samples that change with the picture, the byte budget, and a non-video file.

Risk: additive. Two new tools, one new read-only app method, and one constant raised (8 to 16 images per cmux_exec script, under the same 8 MiB cap).

🤖 Generated with Claude Code

Agent session: agent session


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need.


Summary by cubic

Adds video_info and video_frames tools to the cmux MCP server so agents can inspect a video directly instead of hand-writing ffmpeg commands.

  • video_info {path} returns duration (ms), displayed width and height, fps, and audio presence.
  • video_frames {path, at | range | scenes, count, maxSide} returns frames as MCP image blocks plus a text block listing each frame's time; capped at 16 frames and 6 MiB of base64 JPEG. An over-budget call returns the first frames and says how many it left out, or tells the agent to lower maxSide when the first frame alone is too big.
  • Frame times, sizes, byte budget, and scene changes are picked by the Rust server, with scene sampling starting from the first frame; the macOS app decodes only the asked frames via AVFoundation.
  • The app verifies the file's device and inode, so a chat whose MCP server runs on another machine fails cleanly instead of reading a same-named file on the Mac.
  • Raises cmux_exec's per-script image cap from 8 to 16 so a 16-frame call fits; the 8 MiB byte cap is unchanged.

Written for commit b97dd1b. Summary will update on new commits.

View guided diff Turn on auto-fix

… without ffmpeg

Leo, 2026-11-01: "i feel like cmux-next should give u better tools to deal
with this kind of thing". The server picks times, sizes, the byte budget and
scene changes (Rust); the app decodes through AVFoundation (video.frames).

Agent-Session: https://cmux.com/deeplink/agent?id=aid-bbe6e8ca1bc11f92f25b
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Nov 1, 2026 •

Copy link
Copy Markdown
Contributor

cmux status (one comment, edited in place; each line is updated by the job that owns it)

Review 2e1f291: HQ feed card · gallery before/after runs · checks

App of this head (app.zip, kept 1 day)

gh workflow run reload-build.yml -R manaflow-ai/cmux --ref feat-cmux-next -f tag=pr30921 -f ref=lane/agent-video-frames -f platform=macos -f backend_mode=local

red: cmux-next checks (god files, concurrency, crash safety, l10n); NEW: red check: Script tests (pin-cmux-tui, embed-cef, ensure-cef store) (+1 more) is not red on the base 81b0971; do not merge until green; see lane-rules

Default changes

This pull request changes 9 defaults at b97dd1b382cd (what everyone gets without configuring anything):

  • tunable-source.CmuxNextAgentPane/AgentPaneVideoInfo.swift:isRegularFile: (none) -> "d8d03e9103796c5b22ffb9bd9cfd8fdbeeb2b0852a7eb3e7327c6fedc202344a"
  • tunable-source.CmuxNextAgentPane/AgentPaneVideoInfo.swift:jpeg: (none) -> "5263c6110463866f1da0f3f307bb7ff6804f2ca6acbf900c4e8b7e41b30238ce"
  • tunable-source.CmuxNextAgentPane/AgentPaneVideoInfo.swift:quality: (none) -> "34bc4d2591e1b07c2fa90887cceec0b0cdd0b5619ac94fb05255c8384c518a58"
  • tunable-source.CmuxNextAgentPane/AgentVideoFrames.swift:luma: (none) -> "ae20196d58e67fe5bcc55d6b452afcb7ca9a7d5b2a6876b3a2a5091201f83330"
  • tunable-source.CmuxNextAgentPane/AgentVideoFrames.swift:maximumTimes: (none) -> "de738e7135fbdf40d89d511af8e57bec1481cab2f9879979c0a03b7a736534fd"
  • tunable-source.CmuxNextAgentPane/AgentVideoFrames.swift:read: (none) -> "32a7b97712b578a51d7a18bf78fc8ce8052849f685f109bfe1654b8e8cf09f92"
  • tunable-source.CmuxNextAgentPane/AgentVideoFrames.swift:sides: (none) -> "fa1dbf6b7ee202dfc85c5480dbb548425bf1cb85809bcaa761c82ba9995f5187"
  • tunable-source.CmuxNextApp/Control/VideoControl.swift:maximumBytes: (none) -> "2e0d1fa6bbcc92ae020b34c6b9afe396955e8eb6065fad44937a194635578406"
  • tunable-source.CmuxNextApp/Control/VideoControl.swift:run: (none) -> "118b704cc453cf8e3acdc4911f2daecd6e38e7cf766a6f468d74c9cc78e660e9"

Needs a review. It passes with an independent review receipt on this commit: a comment or review whose first line is Review (<reviewer>): SHIP at b97dd1b382cd9a9baa5822d0471227e7de6179a1, from a member, not a self-review (the newest verdict naming the head decides; a later BLOCK beats an earlier SHIP), or with an approving review on this commit from @lawrencecchen or @austinywang (the call owners in .github/defaults-call-owners.json, never the author). A receipt posted as a review re-runs this check; after a plain comment, re-run it. The needs a call label alone does not pass, and a new push needs a new receipt.

Merge receipt: merged as af670df60fb0 with these defaults changed, WITHOUT a SHIP receipt or an approving call owner review on its head.

Regenerate the snapshot with python3 scripts/ci/defaults_guard.py snapshot.

Shipped in:

@github-actions github-actions Bot added the risk:normal Risk tier (docs/ci/risk-tiers.md) label Nov 1, 2026
@teamleaderleo

Copy link
Copy Markdown
Collaborator Author

Owned by an agent session · session

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

9 issues found across 8 files

You’re at about 94% of the monthly reviewed-line limit. You may want to disable incremental reviews to conserve quota. Reviews will continue until that limit is exceeded. If you need help avoiding interruptions, please contact contact@cubic.dev.

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="cmux-tui/crates/cmux-tui/src/cli/mcp/video_tools.rs">

<violation number="1" location="cmux-tui/crates/cmux-tui/src/cli/mcp/video_tools.rs:320">
P3: `scenes: true, count: 1` always reports that the picture barely changes, even when scene samples contain large changes. Remove that claim or pass whether any change was detected separately from the selected frame count.</violation>

<violation number="2" location="cmux-tui/crates/cmux-tui/src/cli/mcp/video_tools.rs:370">
P3: When the file is missing on this machine, the error only says 'cannot read ...'. The module doc says remote chats are told the file is not on this Mac and to use that machine's tools, so this message should say the file is not on this machine and point to that machine's tools.</violation>
</file>

<file name="Packages/macOS/CmuxNext/Tests/CmuxNextAgentPaneTests/AgentVideoFramesTests.swift">

<violation number="1" location="Packages/macOS/CmuxNext/Tests/CmuxNextAgentPaneTests/AgentVideoFramesTests.swift:28">
P2: A writer failure can make this loop spin forever: the helper records failures with `#expect` but never aborts or checks `writer.status`. Use throwing requirements for writer startup/appends and bound or status-check the readiness wait so codec or disk failures fail the test rather than hanging CI.</violation>

<violation number="2" location="Packages/macOS/CmuxNext/Tests/CmuxNextAgentPaneTests/AgentVideoFramesTests.swift:88">
P2: Guard the returned frame counts with `try #require` before subscripting; otherwise a decode failure crashes the test process instead of reporting which expectation failed. Apply the same protection to the `frames`, `samples`, and `one.frames` subscripts in this suite.</violation>
</file>

<file name="cmux-tui/crates/cmux-tui/src/cli/mcp/code_mode.rs">

<violation number="1" location="cmux-tui/crates/cmux-tui/src/cli/mcp/code_mode.rs:994">
P2: In one script, a second `video_frames` call (or a call after a computer use screenshot) can exceed the shared 8 MiB image budget. Its frames then come back as `dropped: true` markers, and the call's text still says the frames are 'below'. Surface the script-level drop in the text, or count the video budget against the shared image budget before answering.</violation>
</file>

<file name="Packages/macOS/CmuxNext/Sources/CmuxNextApp/Control/VideoControl.swift">

<violation number="1" location="Packages/macOS/CmuxNext/Sources/CmuxNextApp/Control/VideoControl.swift:35">
P2: The identity check is not bound to the file AVFoundation later opens. A pathname can be replaced after `stat`, causing `video.frames` to decode a different file; open and validate a descriptor, or decode a verified snapshot instead.</violation>

<violation number="2" location="Packages/macOS/CmuxNext/Sources/CmuxNextApp/Control/VideoControl.swift:51">
P2: Wrong-typed params fall back silently here. A fractional `side` such as 1280.5 becomes 1280, and a non-array `times` returns info with no frames and no error. Reject these with `invalid_params`, as the element checks for `times` already do. The Rust caller validates its own values, so this matters mainly for other callers.</violation>
</file>

<file name="Packages/macOS/CmuxNext/Sources/CmuxNextAgentPane/AgentVideoFrames.swift">

<violation number="1" location="Packages/macOS/CmuxNext/Sources/CmuxNextAgentPane/AgentVideoFrames.swift:88">
P2: `read` returns a report with no frames when no requested frame decodes, so `video.frames` answers success instead of `unsupported`. Return nil when `report.frames` is empty, as `AgentPaneVideoInfo.read` does. Failed decodes are also skipped with no count, so the agent cannot tell a failed frame from one that was never asked for.</violation>

<violation number="2" location="Packages/macOS/CmuxNext/Sources/CmuxNextAgentPane/AgentVideoFrames.swift:98">
P2: `luma` may read the CGContext's backing buffer after the context is released. Swift does not guarantee the context lives past its last use, so keep it alive until the bytes are copied.</violation>
</file>

Reply with feedback, questions, or to request a fix.

View guided diff | Turn on auto-fix | Re-trigger cubic

#expect(report.frames.map(\.atMs) == [200, 1600])
let frames = try report.frames.map { try Self.image($0.data) }
#expect(frames.allSatisfy { $0.width == 320 && $0.height == 180 })
#expect(try Self.isRed(frames[0]))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: Guard the returned frame counts with try #require before subscripting; otherwise a decode failure crashes the test process instead of reporting which expectation failed. Apply the same protection to the frames, samples, and one.frames subscripts in this suite.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At Packages/macOS/CmuxNext/Tests/CmuxNextAgentPaneTests/AgentVideoFramesTests.swift, line 88:

<comment>Guard the returned frame counts with `try #require` before subscripting; otherwise a decode failure crashes the test process instead of reporting which expectation failed. Apply the same protection to the `frames`, `samples`, and `one.frames` subscripts in this suite.</comment>

<file context>
@@ -0,0 +1,123 @@
+        #expect(report.frames.map(\.atMs) == [200, 1600])
+        let frames = try report.frames.map { try Self.image($0.data) }
+        #expect(frames.allSatisfy { $0.width == 320 && $0.height == 180 })
+        #expect(try Self.isRed(frames[0]))
+        #expect(try !Self.isRed(frames[1]))
+        // A frame is never made larger than the video.
</file context>

kCVPixelBufferWidthKey as String: width, kCVPixelBufferHeightKey as String: height,
])
writer.add(input)
#expect(writer.startWriting(), "\(String(describing: writer.error))")

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: A writer failure can make this loop spin forever: the helper records failures with #expect but never aborts or checks writer.status. Use throwing requirements for writer startup/appends and bound or status-check the readiness wait so codec or disk failures fail the test rather than hanging CI.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At Packages/macOS/CmuxNext/Tests/CmuxNextAgentPaneTests/AgentVideoFramesTests.swift, line 28:

<comment>A writer failure can make this loop spin forever: the helper records failures with `#expect` but never aborts or checks `writer.status`. Use throwing requirements for writer startup/appends and bound or status-check the readiness wait so codec or disk failures fail the test rather than hanging CI.</comment>

<file context>
@@ -0,0 +1,123 @@
+            kCVPixelBufferWidthKey as String: width, kCVPixelBufferHeightKey as String: height,
+        ])
+        writer.add(input)
+        #expect(writer.startWriting(), "\(String(describing: writer.error))")
+        writer.startSession(atSourceTime: .zero)
+        for index in 0..<10 {
</file context>

/// Images a script's calls returned that one `cmux_exec` result carries
/// (a computer use screenshot), and their total base64 bytes.
const MAX_IMAGES: usize = 8;
/// (a computer use screenshot, a `video_frames` call's frames), and their

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: In one script, a second video_frames call (or a call after a computer use screenshot) can exceed the shared 8 MiB image budget. Its frames then come back as dropped: true markers, and the call's text still says the frames are 'below'. Surface the script-level drop in the text, or count the video budget against the shared image budget before answering.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At cmux-tui/crates/cmux-tui/src/cli/mcp/code_mode.rs, line 994:

<comment>In one script, a second `video_frames` call (or a call after a computer use screenshot) can exceed the shared 8 MiB image budget. Its frames then come back as `dropped: true` markers, and the call's text still says the frames are 'below'. Surface the script-level drop in the text, or count the video budget against the shared image budget before answering.</comment>

<file context>
@@ -987,8 +991,9 @@ fn closed() -> Value {
 /// Images a script's calls returned that one `cmux_exec` result carries
-/// (a computer use screenshot), and their total base64 bytes.
-const MAX_IMAGES: usize = 8;
+/// (a computer use screenshot, a `video_frames` call's frames), and their
+/// total base64 bytes.
+const MAX_IMAGES: usize = super::video_tools::MAX_FRAMES;
</file context>

guard let format = AgentVideoFrames.Format(rawValue: raw) else { throw ControlError.invalidParams("video.frames format is jpeg or luma") }
return format
} ?? .jpeg
let side = params["side"]?.intValue ?? 1280

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: Wrong-typed params fall back silently here. A fractional side such as 1280.5 becomes 1280, and a non-array times returns info with no frames and no error. Reject these with invalid_params, as the element checks for times already do. The Rust caller validates its own values, so this matters mainly for other callers.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At Packages/macOS/CmuxNext/Sources/CmuxNextApp/Control/VideoControl.swift, line 51:

<comment>Wrong-typed params fall back silently here. A fractional `side` such as 1280.5 becomes 1280, and a non-array `times` returns info with no frames and no error. Reject these with `invalid_params`, as the element checks for `times` already do. The Rust caller validates its own values, so this matters mainly for other callers.</comment>

<file context>
@@ -0,0 +1,66 @@
+            guard let format = AgentVideoFrames.Format(rawValue: raw) else { throw ControlError.invalidParams("video.frames format is jpeg or luma") }
+            return format
+        } ?? .jpeg
+        let side = params["side"]?.intValue ?? 1280
+        guard AgentVideoFrames.sides.contains(side) else {
+            throw ControlError.invalidParams("video.frames side is \(AgentVideoFrames.sides.lowerBound) to \(AgentVideoFrames.sides.upperBound)")
</file context>

space: CGColorSpaceCreateDeviceGray(), bitmapInfo: CGImageAlphaInfo.none.rawValue) else { return nil }
context.interpolationQuality = .medium
context.draw(image, in: CGRect(x: 0, y: 0, width: side, height: side))
guard let pixels = context.data else { return nil }

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: luma may read the CGContext's backing buffer after the context is released. Swift does not guarantee the context lives past its last use, so keep it alive until the bytes are copied.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At Packages/macOS/CmuxNext/Sources/CmuxNextAgentPane/AgentVideoFrames.swift, line 98:

<comment>`luma` may read the CGContext's backing buffer after the context is released. Swift does not guarantee the context lives past its last use, so keep it alive until the bytes are copied.</comment>

<file context>
@@ -0,0 +1,101 @@
+                                      space: CGColorSpaceCreateDeviceGray(), bitmapInfo: CGImageAlphaInfo.none.rawValue) else { return nil }
+        context.interpolationQuality = .medium
+        context.draw(image, in: CGRect(x: 0, y: 0, width: side, height: side))
+        guard let pixels = context.data else { return nil }
+        return Data(bytes: pixels, count: side * side)
+    }
</file context>

let at = frame.actualTime.seconds.isFinite ? frame.actualTime.seconds : asked
report.frames.append(Frame(atMs: Int((min(max(at, 0), seconds) * 1000).rounded()), data: encoded))
}
return report

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: read returns a report with no frames when no requested frame decodes, so video.frames answers success instead of unsupported. Return nil when report.frames is empty, as AgentPaneVideoInfo.read does. Failed decodes are also skipped with no count, so the agent cannot tell a failed frame from one that was never asked for.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At Packages/macOS/CmuxNext/Sources/CmuxNextAgentPane/AgentVideoFrames.swift, line 88:

<comment>`read` returns a report with no frames when no requested frame decodes, so `video.frames` answers success instead of `unsupported`. Return nil when `report.frames` is empty, as `AgentPaneVideoInfo.read` does. Failed decodes are also skipped with no count, so the agent cannot tell a failed frame from one that was never asked for.</comment>

<file context>
@@ -0,0 +1,101 @@
+            let at = frame.actualTime.seconds.isFinite ? frame.actualTime.seconds : asked
+            report.frames.append(Frame(atMs: Int((min(max(at, 0), seconds) * 1000).rounded()), data: encoded))
+        }
+        return report
+    }
+
</file context>

throw ControlError.invalidParams("video.frames needs dev and ino: the file as the caller sees it")
}
var info = stat()
guard stat(path, &info) == 0, String(UInt64(bitPattern: Int64(info.st_dev))) == dev, String(info.st_ino) == ino else {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: The identity check is not bound to the file AVFoundation later opens. A pathname can be replaced after stat, causing video.frames to decode a different file; open and validate a descriptor, or decode a verified snapshot instead.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At Packages/macOS/CmuxNext/Sources/CmuxNextApp/Control/VideoControl.swift, line 35:

<comment>The identity check is not bound to the file AVFoundation later opens. A pathname can be replaced after `stat`, causing `video.frames` to decode a different file; open and validate a descriptor, or decode a verified snapshot instead.</comment>

<file context>
@@ -0,0 +1,66 @@
+            throw ControlError.invalidParams("video.frames needs dev and ino: the file as the caller sees it")
+        }
+        var info = stat()
+        guard stat(path, &info) == 0, String(UInt64(bitPattern: Int64(info.st_dev))) == dev, String(info.st_ino) == ino else {
+            throw ControlError(code: "not_found", message: "\(path) is not a file on the Mac the cmux app runs on; a chat on another machine reads its videos with that machine's own tools")
+        }
</file context>

fn identity(path: &str) -> Result<(String, Value), String> {
use std::os::unix::fs::MetadataExt as _;
let real =
std::fs::canonicalize(path).map_err(|error| format!("cannot read {path}: {error}"))?;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P3: When the file is missing on this machine, the error only says 'cannot read ...'. The module doc says remote chats are told the file is not on this Mac and to use that machine's tools, so this message should say the file is not on this machine and point to that machine's tools.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At cmux-tui/crates/cmux-tui/src/cli/mcp/video_tools.rs, line 370:

<comment>When the file is missing on this machine, the error only says 'cannot read ...'. The module doc says remote chats are told the file is not on this Mac and to use that machine's tools, so this message should say the file is not on this machine and point to that machine's tools.</comment>

<file context>
@@ -0,0 +1,781 @@
+fn identity(path: &str) -> Result<(String, Value), String> {
+    use std::os::unix::fs::MetadataExt as _;
+    let real =
+        std::fs::canonicalize(path).map_err(|error| format!("cannot read {path}: {error}"))?;
+    let meta = std::fs::metadata(&real).map_err(|error| format!("cannot read {path}: {error}"))?;
+    if !meta.is_file() {
</file context>

text.push_str(&format!(" (change {change:.1})"));
}
}
if scenes && frames.len() == 1 {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P3: scenes: true, count: 1 always reports that the picture barely changes, even when scene samples contain large changes. Remove that claim or pass whether any change was detected separately from the selected frame count.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At cmux-tui/crates/cmux-tui/src/cli/mcp/video_tools.rs, line 320:

<comment>`scenes: true, count: 1` always reports that the picture barely changes, even when scene samples contain large changes. Remove that claim or pass whether any change was detected separately from the selected frame count.</comment>

<file context>
@@ -0,0 +1,781 @@
+            text.push_str(&format!(" (change {change:.1})"));
+        }
+    }
+    if scenes && frames.len() == 1 {
+        text.push_str(
+            "\nThe picture barely changes: ask for range or count frames spread over it instead.",
</file context>

@teamleaderleo

Copy link
Copy Markdown
Collaborator Author

Review (codex gpt-6.1-sol xhigh, router-review): BLOCK at af1a078

BLOCK at af1a078

Visual verdict: no images; Leo tries the build

  1. [P1] [blocking] AgentVideoFrames.swift:49: The public read method exposes Foundation’s URL, but Foundation is imported internally. This target enables InternalImportsByDefault, so Swift rejects the declaration and the app module fails to compile. Change the import to public import Foundation.

  2. [P2] [follow-up] video_tools.rs:251: Scene sampling omits the promised first frame. For a one-hour video, its first sample is at 28.125 seconds; an introduction ending at 20 seconds disappears entirely, and a static remainder produces “The picture barely changes.” Include time zero among the scene samples.

Rust and Swift tests could not run because neither toolchain is installed.

Follow-ups:

…es in video.frames

Agent-Session: https://cmux.com/deeplink/agent?id=aid-bbe6e8ca1bc11f92f25b
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ports Foundation publicly

Review (gpt-6.1-sol) at af1a078: the public read(URL) needs public import Foundation under
InternalImportsByDefault, and scene sampling started half a step in (28 s into an hour).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-bbe6e8ca1bc11f92f25b
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-bbe6e8ca1bc11f92f25b
@blacksmith-sh

blacksmith-sh Bot commented Nov 1, 2026 •

Copy link
Copy Markdown

Found 4 test failures on Blacksmith runners:

Failures

Test View Logs
test_the_workflow_runs_for_every_included_file_outside_cmux_tui (main.Scope.test_th
e_workflow_runs_for_every_included_file_outside_cmux_tui) (event='pull_request', path='
schemas/link-token/native.json')
View Logs
test_the_workflow_runs_for_every_included_file_outside_cmux_tui (main.Scope.test_th
e_workflow_runs_for_every_included_file_outside_cmux_tui) (event='pull_request', path='
schemas/link-token/vectors.json')
View Logs
test_the_workflow_runs_for_every_included_file_outside_cmux_tui (main.Scope.test_th
e_workflow_runs_for_every_included_file_outside_cmux_tui) (event='push', path='schemas/
link-token/native.json')
View Logs
test_the_workflow_runs_for_every_included_file_outside_cmux_tui (main.Scope.test_th
e_workflow_runs_for_every_included_file_outside_cmux_tui) (event='push', path='schemas/
link-token/vectors.json')
View Logs

Fix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need.

@teamleaderleo

Copy link
Copy Markdown
Collaborator Author

Review (codex gpt-6-astra high, router-review): BLOCK at a971d85

BLOCK at a971d85

Visual verdict: no images; Leo tries the build

  1. [P1][blocking] video_tools.rs:464 moves request.pick by matching it by value. Lines 503 and 530 then use request.pick again. Since Pick contains a Vec and is not Copy, this is a Rust “use of moved value” compile error.

  2. [P2][follow-up] video_tools.rs:308-310 returns before reporting dropped. If the first JPEG exceeds the 6 MiB budget, the app returns zero frames with all frames dropped, but the tool incorrectly says no frame could be decoded instead of explaining the budget omission.

Follow-ups:

  • Fix the moved selector ownership issue.
  • Preserve the budget omission message when no frames fit.

Review (gpt-6-astra) at a971d85.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-bbe6e8ca1bc11f92f25b
@teamleaderleo

Copy link
Copy Markdown
Collaborator Author

Review (codex, router-review): SHIP at 2e1f291

Reviewer: codex gpt-6-astra high, fast tier.

SHIP at 2e1f291

Visual verdict: no images; Leo tries the build

Follow-ups: none

cmux-bot[bot]
cmux-bot Bot previously approved these changes Nov 1, 2026

@cmux-bot cmux-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

cmux-bot approves 2e1f291: an independent review passed this exact head (router-review receipt by teamleaderleo). A push needs a new review; a later BLOCK at this head dismisses this approval.

@cmux-bot
cmux-bot Bot temporarily deployed to ci-routing-agent November 1, 2026 14:20 Inactive
@cmux-bot
cmux-bot Bot temporarily deployed to ci-routing-agent November 1, 2026 14:20 Inactive
@cmux-bot
cmux-bot Bot dismissed their stale review November 1, 2026 14:24

A newer verdict blocks b97dd1b (no independent SHIP at the head).

@teamleaderleo
teamleaderleo merged commit af670df into feat-cmux-next Nov 1, 2026
153 of 169 checks passed
@teamleaderleo
teamleaderleo deleted the lane/agent-video-frames branch November 1, 2026 14:31
@lawrencecchen

Copy link
Copy Markdown
Contributor

This PR added clippy::indexing_slicing hits to cmux-tui production code, and with the other culprit (#30921 / #30923) they took the panic ratchet on feat-cmux-next red (cmux-tui: clippy::indexing_slicing 855 > baseline 831). The fix is in #31097: each new index read now uses a checked form (Value::get, JsonSet::set, buffer.get(..read)), with no behavior change and no baseline raise.

lawrencecchen added a commit that referenced this pull request Nov 1, 2026
…d forms (#31097)

#30921 (af670df, cmux mcp video_frames/video_info) and #30923
(b91b9c7, cmux link seed pull) added clippy::indexing_slicing hits in
cmux-tui production code, a deny lint of the panic ratchet: cmux-tui went
from 831 to 855. Each read now uses a checked form; valid input behaves the
same, and a missing or non-object field reads as it did through serde_json's
Index (null):

- video_tools: the app's answer and error fields through Value::get; the
  params writes through cmux_json::JsonSet::set (the params are always an
  object, as before).
- seed_pull export: the job and status fields through Value::get; the
  bundle copy's chunk through buffer.get(..read), an error if a read ever
  claims more than its buffer.
- seed_cli pull keep: summary.base.{head,index,work} through Value::get.

No allow attributes, no baseline change.

Co-authored-by: cmux-bot[bot] <348531757+cmux-bot[bot]@users.noreply.github.com>
lawrencecchen added a commit that referenced this pull request Nov 1, 2026
…l, RoomMembership, IncognitoSpaceMarks out of CmuxNextApp (#29184 #30921 #30666 #30881 #30828) (#31036)

* base red: arch app_lines, move five App files into their packages

The feat-cmux-next tip is red on the arch ratchet's app_lines row: +295
CmuxNextApp lines over the last green arch tip 73dc0d8. This moves
five self-contained App files into the package that owns their domain,
with no behavior change (public API, the App calls it):

- CloudMoveAlwaysGrants (#29184) and CloudMoveCLI -> CmuxNextCloud/Move,
  beside CloudMoveFolder and CloudMoveRetries. CmuxNextCloud now depends
  on CmuxNextDaemon for ProcessRunner (both service layer).
- VideoControl (#30921) -> CmuxNextAgentPane, beside AgentVideoFrames.
  A struct with an instance methods() (namespace convention); the App
  registers VideoControl().methods(). CmuxNextAgentPane now depends on
  CmuxNextControl (ui -> service).
- RoomMembership (#30666 added isHidden) -> CmuxNextDaemon/Store, beside
  PersonalStore whose pins it reads. A pure value type.
- IncognitoSpaceMarks (#30881) -> CmuxNextBridge, beside
  SidebarMapping+Spaces that takes its hidden set. Still MainActor
  (Bridge has the UI default isolation).

Tests that name the moved types import their new module.

* base red: arch global_mutable_statics, IncognitoChats' pending purges are the app's instance (#30828)

#30828 added 'private static var pending' to the IncognitoChats enum,
which raises the arch ratchet's global_mutable_statics row (+1,
CmuxNextApp). IncognitoChats is now a final class that AppServices owns
(services.incognitoChats); WorkspaceClose.willClose and the quit's
windows and endLocalAgents steps call that one instance. The pure
key(of:key:incognito:) stays static. No behavior change: one app, one
set of pending purges, drained at quit as before.

This branch was previously deployed

3 inactive deployments
artifacts — b97dd1b3 Deployed Nov 1, 2026 by cmux-bot[bot] via add the Windows daemon to the tree #96510
sentry-dsyms — b97dd1b3 Deployed Nov 1, 2026 by cmux-bot[bot] via upload cmux-tui dSYMs to Sentry #96510
ci-routing-agent — b97dd1b3 Deployed Nov 1, 2026 by cmux-bot[bot] via cmux-tui tree preflight #96510
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

risk:normal Risk tier (docs/ci/risk-tiers.md)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants