Skip to content

Add model-aware video attachments and fix streaming Markdown recovery - #4585

Closed
reachjalil wants to merge 3 commits into
devfrom
feature/model-video-attachments
Closed

reachjalil wants to merge 3 commits into
devfrom
feature/model-video-attachments

Conversation

@reachjalil

Copy link
Copy Markdown
Member

Summary

  • Send native video only when the exact connected model advertises video input and uses a supported Google/Vertex adapter. Reject incompatible selections before upload and keep the draft, including queued and split-workspace sends.
  • Play attached videos in the thread with native controls and no autoplay. Share authenticated workspace-video loading with Markdown, preserve binary response bytes across Desktop IPC, and return video content types from the server.
  • Recover active transcript snapshots at the real server.connected handshake. The SDK returns a lazy stream iterator before the connection opens; missing initial message declarations could otherwise assign assistant deltas an inferred user role and delay proper Markdown until completion.
  • Show the useful send error on empty threads instead of a generic Error waiting state.

Verification

Overall verdict: Incomplete. Both focused local journeys passed on head 352a639926faa8bc0366bb1e843f5ff8819557f5; remote Desktop playback still needs end-to-end proof. This PR is ready for review, not a release assertion.

Passed

Both commands printed placement: local (--local), cold-started their isolated resources, and exited 0 with 1 passed, 0 failed, 0 skipped each:

OPENWORK_EVAL_ENGINE=v1 pnpm evals:e2e streamed-markdown-answer --local
OPENWORK_EVAL_ENGINE=v1 pnpm evals:e2e attachment-upload-loading-state --local

The streaming journey now targets the mounted composer, verifies the actual sent bubble rather than composer text, and records the first frame for each block. It requires the heading to appear before the closing paragraph and forbids raw Markdown under either message role throughout the stream. It also checks continuity, reload, and video playback. Focus emulation makes trusted input work in background Electron; the cold-start wait is separate from the per-frame rendering assertion.

The attachment journey checks incompatible-model draft preservation, exact video MIME/base64 received by a mock native Google endpoint, authenticated workspace playback, no duplicate players or autoplay, and playback after reload.

Supporting checks passed: app typecheck; 63 focused app tests (attachments, IPC, Markdown, message rendering, and handshake isolation); 16 server path/MIME tests; 15 binary-transfer/mock-provider tests; changed-spec channel ratchet and diff checks. Screenshots are supplementary, not the source of the verdict.

Failed — reproduced on clean base

bun test --isolate tests/prompt-file-parts.test.ts exits 1 with 20 passed, 1 failed on both this branch and clean base 9a64fe1087aff7f5c552a446595d1325e1d1a91b. The unchanged app-mention assertion expects computer-use tools, while the current instruction names computer_discover and computer_open_session. That unrelated wording mismatch is not changed here.

Skipped / deferred

  • No skips in the two focused journeys.
  • Remote Desktop in-thread playback has byte-preservation checks but no full remote end-to-end run yet.
  • Native video in the experimental engine, progressive/range streaming, transcoding, and large-video Files-API transport are outside this change. Workspace videos download fully before playback; codec and provider limits still apply.
  • Provider traffic in the journeys is mocked; no live-provider inference or deployment was performed.

Exact-head evidence for both journeys is attached in the PR comments.

@vercel

vercel Bot commented Sep 6, 2026 •

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
openwork-app Ready Ready Preview Sep 6, 2026 10:30pm UTC
openwork-den Ready Ready Preview Sep 6, 2026 10:30pm UTC
openwork-diagnostics Ready Ready Preview Sep 6, 2026 10:30pm UTC
openwork-landing Ready Ready Preview, v0 Sep 6, 2026 10:30pm UTC

@reachjalil

reachjalil commented Sep 6, 2026 •

Copy link
Copy Markdown
Member Author

Test evidence — a-streaming-answer-renders-as-markdown-block-by-block-and-settles-to-the-same-do — ✅ passed

SHA 352a639 · engine v1

✅ passed · 0 expectations passed · 0 failed · 0 pending

screenshots not attached (gh < 2.99; run brew upgrade gh)

[world] mock(mcp) · den(local) · desktop(as admin) · workspace(/tmp/openwork-streamed-markdown-answer-1788733503885) · [seed:raw] evalIn() ×5 · session("New task") · [seed:raw] evalIn()
[probe:raw] [probe:raw] eval()
[user] type(composer, "Write the streamed markdown answer. STREAM_MARKDOWN_ANSWER")
[probe] composer · eventually(composer is ready to send the typed draft)
[user] Run task · click(Run task)
[probe:raw] [probe:raw] eval() ×3
[probe] eventually(sent text appears in the transcript, not just the composer)
[user] see(text=Streamed answer heading, timeoutMs=90000) · see(text=Closing paragraph epsilon., timeoutMs=120000)
[probe:raw] [probe:raw] eval()
[user] notSee(text=## Streamed) · see(text=Closing paragraph epsilon., timeoutMs=120000) · see(Run task, timeoutMs=60000)
[probe] text
[user] see(text=alpha list item) · see(text=const streamed = "delta";) · notSee(text=/Something went wrong/)
[probe:raw] [probe:raw] eval()
[user] reload · see(text=Closing paragraph epsilon., timeoutMs=120000)
[probe] text
[user] see(text=Write the streamed markdown answer. STREAM_MARKDOWN_ANSWER) · see(text=Streamed answer heading)
[probe:raw] [probe:raw] eval()
steps 1 ✅ finished blocks render as markdown while later blocks are still arriving (10.1s) · 2 ✅ the settled answer shows every block exactly once and no markdown syntax (3.0s) · 3 ✅ sent text and streamed blocks never disappear or duplicate (0.0s) · 4 ✅ a referenced workspace video has controls and plays only when requested (0.0s) · 5 ✅ history renders the same document after a reload (2.8s)
verdict passed · 11 user observations (see ×9, notSee ×2) · 12 probes · steps 5/5


Test run created 2026-09-06T22:24:08.223Z · Source: evals/results/test-runs/2026-09-06T22-24-08-223Z-a-streaming-answer-renders-as-markdown-block-by-block-and-settles-to-the-same-do/test-run.json · Repro: pnpm --dir evals artifacts:publish -- --pr 4585 --test-run 2026-09-06T22-24-08-223Z-a-streaming-answer-renders-as-markdown-block-by-block-and-settles-to-the-same-do


Test evidence — video-keeps-the-draft-on-incompatible-models-sends-intact-with-the-image-on-goog — ✅ passed

SHA 352a639 · engine v1

✅ 0/5 screenshots passed · 0 expectations passed · 0 failed · 0 pending

screenshots not attached (gh < 2.99; run brew upgrade gh)

[world] mock(mcp) · den(local) · desktop(as admin) · workspace(/tmp/openwork-attachment-upload-1788733589316) · [seed:raw] evalIn() ×4 · ❌ session("New task") · session("New task") · [seed:raw] evalIn()
[user] type(composer, "Describe the attached image.")
[seed:raw] [seed:raw] evalIn()
[user] screenshot · see(text=assistant-video.mp4)
[probe] composer
[user] label=Change model · click(label=Change model) · label=/^Model\s/ · click(label=/^Model\s/) · type(placeholder=Search models..., "Attachment image-only model", replace) · screenshot · label=/Attachment image-only model/ · click(label=/Attachment image-only model/) · see(label=Change model, text=/Attachment image-only model/) · Run task · click(Run task) · see(text=This model connection does not support video input.) · see(composer, editable, text=/Describe the attached image./)
[probe] composer
[user] notSee(text=/1 queued/) · label=Dismiss error · click(label=Dismiss error) · notSee(text=This model connection does not support video input.)
[probe] composer · eventually(retained draft can be sent after dismissing the error)
[user] label=Change model · click(label=Change model) · label=/^Model\s/ · click(label=/^Model\s/) · type(placeholder=Search models..., "Attachment unsupported-video model", replace) · screenshot · label=/Attachment unsupported-video model/ · click(label=/Attachment unsupported-video model/) · see(label=Change model, text=/Attachment unsupported-video model/) · Run task · click(Run task) · see(text=This model connection does not support video input.) · see(composer, editable, text=/Describe the attached image./)
[probe] composer
[user] notSee(text=/1 queued/) · label=Dismiss error · click(label=Dismiss error) · notSee(text=This model connection does not support video input.)
[probe] composer · eventually(retained draft can be sent after dismissing the error)
[user] label=Change model · click(label=Change model) · label=/^Model\s/ · click(label=/^Model\s/) · type(placeholder=Search models..., "Attachment video model", replace) · label=/Attachment video model/ · click(label=/Attachment video model/) · see(label=Change model, text=/Attachment video model/) · see(composer, editable, text=/Describe the attached image./)
[probe] composer
[seed:raw] [seed:raw] evalIn()
[user] Run task · click(Run task)
[probe:raw] [probe:raw] eval()
[probe] eventually(attachment uploading state observed)
[user] notSee(text=/1 queued/)
[probe] hash
[probe:raw] [probe:raw] eval()
[probe] eventually(attachment cleanup after send)
[user] see(text=attachment upload loading proof, timeoutMs=90000)
[probe] eventually(native Google completion with attachments) · composer
[user] notSee(text=/Cannot read binary file|UnsupportedFunctionalityError/) · see(text=assistant-video.mp4)
[probe] eventually(attachment decoded without autoplay) · eventually(video playback advances) · eventually(workspace reference decoded without autoplay) · eventually(video playback advances)
[user] screenshot · reload · see(text=attachment upload loading proof, timeoutMs=60000) · see(text=assistant-video.mp4)
[probe] eventually(attachment decoded without autoplay) · eventually(video playback advances) · eventually(workspace reference decoded without autoplay) · eventually(video playback advances)
[user] screenshot
steps 1 ✅ manual approval exempts only chat-attachment inbox uploads (0.0s) · 2 ✅ paste a real video alongside the image (0.0s) · 3 ✅ Attachment image-only model rejects video without losing the draft or calling the model (7.0s) · 4 ✅ Attachment unsupported-video model rejects video without losing the draft or calling the model (6.8s) · 5 ✅ select a supported Google model without changing the retained draft (0.3s) · 6 ✅ Google receives the exact video MIME and bytes alongside the image (3.0s) · 7 ✅ sent video has one player with controls and plays only when requested (0.1s) · 8 ✅ reloaded video has one player with controls and plays only when requested (4.5s)
verdict passed · 24 user observations (screenshot ×5, see ×13, notSee ×6) · 23 probes · steps 8/8

⚪ UNVALIDATED — 1. video-keeps-the-draft-on-incompatible-models-sends-intact-with-the-image-on-goog artifact 1

  • ⚪ UNVALIDATED — no visual expectations recorded.

⚪ UNVALIDATED — 2. video-keeps-the-draft-on-incompatible-models-sends-intact-with-the-image-on-goog artifact 2

  • ⚪ UNVALIDATED — no visual expectations recorded.

⚪ UNVALIDATED — 3. video-keeps-the-draft-on-incompatible-models-sends-intact-with-the-image-on-goog artifact 3

  • ⚪ UNVALIDATED — no visual expectations recorded.

⚪ UNVALIDATED — 4. video-keeps-the-draft-on-incompatible-models-sends-intact-with-the-image-on-goog artifact 4

  • ⚪ UNVALIDATED — no visual expectations recorded.

⚪ UNVALIDATED — 5. video-keeps-the-draft-on-incompatible-models-sends-intact-with-the-image-on-goog artifact 5

  • ⚪ UNVALIDATED — no visual expectations recorded.

Test run created 2026-09-06T22:25:33.110Z · Source: evals/results/test-runs/2026-09-06T22-25-33-110Z-video-keeps-the-draft-on-incompatible-models-sends-intact-with-the-image-on-goog/test-run.json · Repro: pnpm --dir evals artifacts:publish -- --pr 4585 --test-run 2026-09-06T22-25-33-110Z-video-keeps-the-draft-on-incompatible-models-sends-intact-with-the-image-on-goog

Comment on lines +56 to 63
// process polls after the heading. A buffered whole answer fails this too.
const frames = await transcript.firstSeenFrames();
const headingFrame = frames[1];
const closingFrame = frames[blockSentinels.length];
expect(headingFrame).toEqual(expect.any(Number));
expect(closingFrame).toBeGreaterThan(headingFrame ?? Number.POSITIVE_INFINITY);
await user.notSee({ text: "## Streamed" });
});

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.

Streaming check allows the answer tail to remain buffered until completion

The replacement assertion only proves that the heading appeared in an earlier frame than the closing text, so a renderer that shows the heading and then buffers every later block until the stream completes would still pass the claimed block-by-block streaming behavior.

Evidence
  • streamedMarkdown emits 8-character chunks with a 1500ms delay between chunks (evals/worlds/chat.ts:766-768), creating a real interval in which later content is still arriving.
  • The changed test waits for closingText before reading firstSeenFrames() and only compares frames[1] with frames[6].
  • A broken renderer can render the heading first, withhold all subsequent blocks, and insert the entire remaining answer at completion; the two first-seen frames are still ordered and all settled-document assertions pass.
  • The previous notSee({ text: closingText }) check was removed, so the test no longer observes that later content is absent while the earlier block is displayed.

Identified by Warden · spec-provenance-review · 4UC-WSU

@diff-warden diff-warden Bot left a comment

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.

Warden security clearance: clear. No blocking findings (security or desktop↔den sync) in this diff (352a639926faa8bc0366bb1e843f5ff8819557f5). Automated clearance satisfies the required-review gate only — a human still reviews and merges. Analysis run

This branch was successfully deployed

4 active deployments
Preview – openwork-landing — 352a6399 Deployed Sep 6, 2026 by vercel[bot]
Preview – openwork-app — 352a6399 Deployed Sep 6, 2026 by vercel[bot]
Preview – openwork-den — 352a6399 Deployed Sep 6, 2026 by vercel[bot]
Preview – openwork-diagnostics — 352a6399 Deployed Sep 6, 2026 by vercel[bot]
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