Skip to content

deck: deck.media review follow-ups (#515) - #517

Merged
ymote merged 3 commits into
mainfrom
fix/deck-media-followups
Oct 13, 2026
Merged

ymote merged 3 commits into
mainfrom
fix/deck-media-followups

Conversation

@ymote

@ymote ymote commented Oct 13, 2026

Copy link
Copy Markdown
Contributor

Why

#515 (deck.media, merged as 083f89f0) came back from review clean on safety, with one minor issue and four nits. This PR fixes all five.

What

  1. MINOR: a link at a clip's name no longer blocks that clip for good (src/media.rs).
    • Before: the clip's path went through crate::inside(), which follows a final link. A link at .cache/media/<hash>.<ext> pointing to a file outside storage was refused as invalid: before the stale-entry repair could run. Eviction, clear, forget and prune only remove regular files, so nothing ever removed the link.
    • Now: the path is the media cache's own folder joined with the service-built name: the hex SHA-256 and an extension from EXTS. The folder is checked to be plain (.cache and .cache/media, links refused, as before). Nothing at that name is resolved.
    • The repair: if anything other than a plain file holding the clip's bytes is at the name, it's removed (a link is removed itself, never followed) and the clip is written anew, create-only. Reuse still accepts only a regular file, so a link is never handed out, even one pointing at the clip's exact bytes.
  2. NIT: eviction spares clips that open documents use. Clips no open document uses go first, even newer ones. An open document's clips go only when that isn't enough, the one used longest ago first. media() reads the set (docs::clips_in_use, now pub(crate)) before the call takes the documents table, since the table is borrowed during the call.
  3. NIT: only what can fit is staged.
    • In deck.media: after the render cache and then the media cache are cleared, a clip still bigger than the room left is storage_full: (the contract's words) before any staging folder is made.
    • In writing.rs::place (save and export): the render-cache clear and the room check now come before staging. What still doesn't fit is refused with the commit's own words (storage_full: the results are N bytes, more than the R bytes left in this storage), and nothing is staged.
    • Both go through a new crate::stage, which counts staging folders in tests (a #[cfg(test)] counter, like ENGINE_READS).
  4. NIT: the 32 MiB cap comes before the hash. A clip larger than the whole media cache is refused first, so a 64 MiB clip is no longer hashed on the UI thread just to be refused. It can't already be in the cache, so the answer is the same.
  5. NIT: wording. SERVICE.md's "Writes are staged and never replace a file the call didn't create" now says that a stale entry at a clip's own name (a file that doesn't hold the clip's bytes, or a link, never followed) is removed and the clip written there anew.

Docs follow the eviction change. SERVICE.md's "Oldest first" as-built bullet adds "Clips no open document uses go before any that one does". The deck paragraph in apps/AGENTS.md describes the eviction order and the refusal before staging. The module doc in src/media.rs covers all five changes.

Tests

On the Linux build host (x86_64), at head ef2c8f79 on main 083f89f0:

  • cargo test --locked -p octosense-deck-service -p octosense-engine-area:
    • deck service lib: 98 passed (96 before, plus 2 new) and 1 ignored (the timing test);
    • first_call and skill: 1 passed each;
    • engine-area: 28 passed.
    • The deck service lib suite then passed three more times in a row.
  • cargo clippy --locked -p octosense-deck-service -p octosense-engine-area --all-targets --no-deps: no findings in either crate.
  • A negative check that wasn't committed: with the old path code (through crate::inside()) put back, the new link test fails with invalid: deck.media: `path` stays inside this app's storage, which is the reported bug.

New and changed, in src/doc_tests.rs:

  • a_link_at_a_clips_name_goes_and_is_never_followed (Unix), the test for the minor issue. A link at the clip's own name points to a file outside the storage. In a second case, the outside file holds the clip's exact bytes. In both cases:

    • deck.media answers that name;
    • the entry there is now a plain file with the clip's bytes;
    • the outside file's bytes and modification time are unchanged;
    • closing the document removes the clip.

    Before this fix, deck.media answered invalid: here.

  • eviction_takes_clips_no_open_document_uses_first: seven clips handed to an open document, then a stray clip no document uses, written last, so the newest.

    • The next write evicts the stray, not the document's oldest clip.
    • The write after that, with only the document's clips left, evicts the one used longest ago.
  • the_render_cache_then_the_media_cache_are_cleared_before_storage_full now also checks the staging counter:

    • a clip that fits is staged once;
    • the over-32 MiB clip and the clip that doesn't fit even after both clears are refused with nothing staged.
  • the_cache_is_cleared_before_storage_full (export, via place) now also checks that the refused export is storage_full: in the commit's own words, with nothing staged.

Not run: no shell, desktop or Quick Deck UI.

🤖 Generated with Claude Code

ymote and others added 3 commits October 13, 2026 12:11
#515's review found one minor issue and four nits.

- A link at a clip's own name, pointing outside storage, blocked that
  clip for good: the path went through crate::inside(), which follows a
  final link and refused before the stale-entry repair ran, and eviction,
  clear, forget and prune remove only regular files. The path is now the
  checked plain .cache/media folder joined with the service-built name (a
  hex SHA-256 and an extension from EXTS), so the repair removes whatever
  is there, a link itself and never what it points to, and writes the
  clip anew.
- Eviction takes clips no open document uses first, and an open
  document's clips only when that is not enough, each the one used
  longest ago first; deck.media reads the set before the call takes the
  documents table.
- Only what can fit is staged: after the render cache and then the media
  cache are cleared, a clip still bigger than the room left is
  storage_full: before any staging, and writing.rs::place (save, export)
  clears the render cache and refuses in the commit's own words before it
  stages. Both stage through crate::stage, counted in tests.
- The 32 MiB cap is checked before the clip is hashed.

New tests: a link at a clip's name (to a file outside storage, and to one
holding the clip's very bytes) goes, the clip is written as a plain file
and the file outside is untouched; eviction takes a newer clip no
document uses before an open document's oldest. The storage_full tests
check that nothing is staged. The deck paragraph in apps/AGENTS.md
follows.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
"Writes are staged and never replace a file the call didn't create" now
says that a stale entry at a clip's own name (a file that doesn't hold
the clip's bytes, or a link, never followed) is removed and the clip
written there anew, as #515's review asked. The as-built "oldest first"
bullet adds that clips no open document uses go before any that one
does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Brings #517 up to main after #512 (updater) and #516 (window full
screen) merged; no conflicts.

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

ymote commented Oct 13, 2026

Copy link
Copy Markdown
Contributor Author

Local CI passed on b81b102 (tools/ci-local.sh --only all --linux-host --offload, 16m09s, Darwin arm64; the ubuntu jobs and the macOS jobs' portable steps (their macOS steps ran here) on the Linux build host, Linux x86_64).

GitHub would run for this PR: desktop.yml, phone.yml, apps.yml. Merged on this local pass (macOS runner queue); GitHub CI runs on the merge commit on main.

Job Step Result Time
ci-local / drift The local mapping fits the workflows PASS -
desktop.yml / desktop Prepare the pinned framework sources PASS 1s
desktop.yml / desktop Compile the desktop with the shared runtime PASS 4m29s
desktop.yml / desktop Desktop tools and scripts PASS 19s
desktop.yml / desktop (linux) Prepare the pinned framework sources PASS 0s
desktop.yml / desktop (linux) One copy of every shell source PASS -
desktop.yml / desktop (linux) In-app updater and installer handoff PASS 1m42s
desktop.yml / desktop (linux) One Makepad, App Hub, octos and Rinx in the locked graph PASS 2s
desktop.yml / desktop (linux) The native apps' generated places match native-apps.json PASS 0s
desktop.yml / native-host-api Prepare the pinned framework sources PASS 1s
desktop.yml / native-host-api Build the native acceptance host and pinned Hub CLI PASS 2m05s
desktop.yml / native-host-api Signed app tool calls a real OS permission API PASS 12s
phone.yml / home Prepare pinned sources and the reviewed runtime patch PASS 12s
phone.yml / home Validate launcher icons and Apple asset catalogs PASS 9s
phone.yml / home (linux) Prepare pinned sources and the reviewed runtime patch PASS 0s
phone.yml / home (linux) Verify the single runtime graph PASS 2s
phone.yml / home (linux) The native apps' generated places match native-apps.json PASS 0s
phone.yml / home (linux) The pinned octos revision PASS 0s
phone.yml / home (linux) Build octos (unless cached) PASS -
phone.yml / home (linux) Build Home, App Hub admission and runtime policy tests PASS 3m22s
phone.yml / home (linux) Test Home, App Hub admission and runtime policy PASS 42s
phone.yml / home (linux) Two-lane scenario (real kernel, scripted model) PASS 27s
phone.yml / home (linux) Build opt-in AppCard tests PASS 2m46s
phone.yml / home (linux) Test opt-in AppCard PASS 3s
phone.yml / home (linux) Build craft-engine relay and system-chat tests PASS 2m48s
phone.yml / home (linux) Test craft-engine relay and system chat PASS 4s
phone.yml / home (linux) Build linked Terminal tests PASS 2m31s
phone.yml / home (linux) Test linked Terminal PASS 1s
phone.yml / home (linux) Build Wasm and runtime policy tests PASS 1m17s
phone.yml / home (linux) Test Wasm and runtime policy PASS 11s
phone.yml / home Compile Home and its bundled modules PASS 8m41s
apps.yml / services (linux) python3 tools/setup.py PASS 0s
apps.yml / services (linux) Shared system app interface PASS 0s
apps.yml / services (linux) Kernel service PASS 18s
apps.yml / services (linux) OctoSense release updater PASS 6s
apps.yml / services (linux) App peers PASS 28s
apps.yml / services (linux) AI providers config PASS 5s
apps.yml / services (linux) Connected provider protocols and Linux host adapter PASS 48s
apps.yml / services (linux) L0 in-card chat (sys.chat, host side) PASS 10s
apps.yml / services (linux) System toolbox (workflow templates) PASS 1m29s
apps.yml / kernel-security (linux) python3 tools/setup.py PASS 0s
apps.yml / kernel-security (linux) The pinned octos revision PASS 0s
apps.yml / kernel-security (linux) Build octos (unless cached) PASS -
apps.yml / kernel-security (linux) Real-kernel tests PASS 40s
apps.yml / apps (linux) python3 tools/setup.py PASS 0s
apps.yml / apps (linux) Host services (Mail, AI providers, News) PASS 2m07s
apps.yml / apps (linux) Connected accounts and Rinx editor PASS 1m30s
apps.yml / apps (linux) Shell AI services (crates/ai-host) PASS 3m01s
apps.yml / apps (linux) AppCard runtime setup safeguards PASS 0s
apps.yml / apps (linux) AppCard clippy (own crates; shared runtime compiles) PASS 27s
apps.yml / apps (linux) AppCard transport and store PASS 8s
apps.yml / apps (linux) One Makepad, App Hub, octos and Rinx in the locked graph PASS 4s
rom.yml / product (linux) python3 -m unittest discover -s tests -v SKIPPED (!) -
rom.yml / product (linux) Check generated Agent Binder client SKIPPED (!) -
rom.yml / product (linux) bash -n scripts/stage-forks.sh scripts/publish-release.sh scripts/apply-to-tree.sh scripts/build-rom.sh PASS -
rom.yml / web-installer (linux) npm ci --ignore-scripts --no-audit --no-fund SKIPPED (!) -
rom.yml / web-installer (linux) npm test SKIPPED (!) -
rom.yml / web-installer (linux) npx playwright install --with-deps chromium SKIPPED (!) -
rom.yml / web-installer (linux) npm run test:browser SKIPPED (!) -
rom.yml / web-installer (linux) python3 -m unittest discover -s tests -p test_installer_manifest.py -v SKIPPED (!) -
rom.yml / web-installer (linux) bash -n scripts/release.sh SKIPPED (!) -
linux-host / sandbox (linux) python3 tools/setup.py PASS 0s
linux-host / sandbox (linux) Process sandbox on Linux (Landlock, seccomp) PASS 1m47s

Skipped (these did NOT pass; (!) marks a skip that blocks tools/ci-local-merge.sh):

  • rom.yml / product (linux): python3 -m unittest discover -s tests -v: javac -version does not work here: install a JDK (17 or newer) and put its bin/ on PATH or set JAVA_HOME
  • rom.yml / product (linux): Check generated Agent Binder client: needs build-tools/35.0.0/aidl, platforms/android-35/framework.aidl in the Android SDK (no Android SDK found): install the SDK packages 'build-tools;35.0.0' and 'platforms;android-35' (sdkmanager) and set ANDROID_HOME
  • rom.yml / web-installer (linux): npm ci --ignore-scripts --no-audit --no-fund: no node, npm, npx on PATH (the workflow uses node 24); put node on PATH
  • rom.yml / web-installer (linux): npm test: no node, npm, npx on PATH (the workflow uses node 24); put node on PATH
  • rom.yml / web-installer (linux): npx playwright install --with-deps chromium: no node, npm, npx on PATH (the workflow uses node 24); put node on PATH
  • rom.yml / web-installer (linux): npm run test:browser: no node, npm, npx on PATH (the workflow uses node 24); put node on PATH
  • rom.yml / web-installer (linux): python3 -m unittest discover -s tests -p test_installer_manifest.py -v: no node, npm, npx on PATH (the workflow uses node 24); put node on PATH
  • rom.yml / web-installer (linux): bash -n scripts/release.sh: no node, npm, npx on PATH (the workflow uses node 24); put node on PATH

Notes:

  • GitHub runs Python 3.12; this run used Python 3.9.6
  • GitHub runs Python 3.12; the Linux host used Python 3.14.4

@ymote
ymote merged commit ffd5e48 into main Oct 13, 2026
1 of 6 checks passed
@ymote
ymote deleted the fix/deck-media-followups branch October 13, 2026 05:09
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