Skip to content

media/02-album-art#33

Draft
SunkenInTime wants to merge 9 commits into
media/01-status-sourceappfrom
media/02-album-art
Draft

media/02-album-art#33
SunkenInTime wants to merge 9 commits into
media/01-status-sourceappfrom
media/02-album-art

Conversation

@SunkenInTime

@SunkenInTime SunkenInTime commented Jul 25, 2026

Copy link
Copy Markdown
Owner

Stack: 02/05

What becomes usable

  • Windows SMTC metadata/session events coalesce into the existing 1 Hz media poll.
  • Optional MediaData.artPath points only at a complete host-owned cache object.
  • Spotify artwork is bounded, normalized to the widget image budget when needed, hash-deduped, atomically published, LRU-pruned, and visibly rendered.
  • Dynamic <image> sources re-register after every mutating JS turn while unchanged static-image trees exit on one generation comparison.
  • Widget assets and host-cache paths are canonicalized and contained with Windows-specific traversal, prefix, drive-relative, UNC, and device-path rejection.

Deliberately missing

  • Media transport commands and the media-transport capability land in 03/05.
  • The macOS MediaRemote adapter remains spike-gated for 04/05.
  • The Noro acceptance port lands in 05/05.

Verification

  • npm test ? PASS, 62/62.
  • npm run typecheck ? PASS.
  • runtime: zig build test -Dweb-layer=exclude -Dtrace=off ? PASS (Zig 0.16.0).
  • host: zig build test ? PASS (Zig 0.16.0).
  • runtime: zig build -Dweb-layer=exclude -Dtrace=off ? PASS.
  • host: zig build ? PASS.
  • node cli/bin/weaver.js check examples/now-playing ? PASS.
  • Native SDK submodule remains unchanged at 3f6a68b606e110087b5992cbe75f700051f1b7f3.

Live and visual gate

Spotify art was captured from the real desktop-layer widget after minimizing other windows. The accepted settled, track-change, and pause PNGs were each reopened at original resolution and graded element-by-element in docs/media-evidence/pr02-visual.md.

  • Settled live art + metadata: PASS.
  • Real next-track change: PASS; the next changed provider frame published and rendered a distinct 256x256 cache object.
  • Real pause status flip: PASS; art and metadata remained correct.
  • The initial empty-art capture was rejected. Runtime evidence identified ImageTooLarge for Spotify's 300x300 source; bounded host normalization repaired it before the accepted captures.

Performance claim

No observed static-image idle CPU regression. With identical ReleaseFast examples/styling-images builds and matched ~60.014 s windows after 41?42 s settles:

  • Parent 2feb700: 0.000 ms CPU; private 42.719 -> 42.684 MiB.
  • Head implementation 8f66767: 0.000 ms CPU; private 42.758 -> 41.570 MiB.

Raw values and the deliberately bounded claim are recorded in docs/media-evidence/pr02-visual.md; no generalized memory claim is made.

Adversarial review update (2026-07-25)

F9 is fixed at this owning layer. A transient SMTC thumbnail read, normalization, size, or cache-publication failure now retains the prior art path and cache pin; only a successful replacement or a confirmed no-art state clears it. The new retention/no-art test passes, as do the full layer gates: npm 62/62, typecheck, runtime/host zig build test, production builds, and weaver check examples/now-playing.

Summary by CodeRabbit

  • New Features

    • Added album artwork support to media data, widgets, and runtime image rendering.
    • Added persistent artwork caching with size limits, deduplication, cleanup, and retention of previously valid artwork during temporary refresh failures.
    • Added secure validation for artwork paths and dynamic image updates.
    • Updated the Now Playing widget with improved artwork, placeholder, sizing, and progress layouts.
  • Documentation

    • Documented the media artwork contract, cache behavior, validation rules, and visual evidence results.
  • Bug Fixes

    • Improved media update efficiency through event coalescing and resolved oversized artwork handling.

Final artwork association review (2026-07-25)

Final implementation head a153e6c; GitHub Actions run 30193379848 is PASS for gate, Intel headless, Apple-silicon headless, and hosted Apple-silicon. Greptile is PASS.

A replacement media item is now atomic with its artwork outcome. Transient refresh failure retains the prior complete metadata+art frame and retries only on the existing subscription-driven 1 Hz poll. Oversized art resolves immediately as unavailable; other unresolved refreshes release refreshed metadata artless after three attempts while retaining the previous cache path/pin. Later subscribed retries can still recover and publish art. Tests cover the retry truth table, three-attempt bound, cache-pin retention, cross-session suppression, and permanent-unavailable outcome.

The final Windows visual recheck delivered a real Spotify Next command and viewed five exact-region captures: the old title/cover remained paired at 250/750 ms and the replacement title/cover appeared together at 1500 ms, with no blank, black, fallback, or mismatched frame. Evidence is in docs/media-evidence/pr05-visual.md.

@coderabbitai

coderabbitai Bot commented Jul 25, 2026

Copy link
Copy Markdown
🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title is related to album art support but is too terse and branch-like to clearly describe the change. Rename it to a concise descriptive sentence such as 'Add album artwork support to the media layer.'
✅ Passed checks (3 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch media/02-album-art

Comment @coderabbitai help to get the list of available commands.

@SunkenInTime
SunkenInTime force-pushed the media/02-album-art branch 2 times, most recently from ec77763 to 3a492d6 Compare July 25, 2026 19:50
@greptile-apps

greptile-apps Bot commented Jul 26, 2026

Copy link
Copy Markdown

Greptile Summary

Adds end-to-end Windows album-art support for media widgets.

  • Coalesces SMTC session and metadata events into the existing media polling flow, with bounded retries and session-aware artwork resolution.
  • Normalizes, deduplicates, atomically publishes, pins, and prunes host-owned artwork cache entries.
  • Validates runtime image paths and re-registers dynamically changing image sources.
  • Extends the media contract and Now Playing example with optional artwork rendering.
  • Adds tests, documentation, and live visual evidence for artwork, track changes, pause behavior, and static-image idle performance.

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains; the previously reported session-removal, transient-refresh, oversized-artwork, and cross-session artwork paths now resolve without exposing stale artwork or indefinitely withholding replacement metadata.

Important Files Changed

Filename Overview
host/src/windows_providers.cpp Adds SMTC dirty-event coalescing, thumbnail reading and normalization, bounded refresh retries, and explicit session/artwork outcomes.
host/src/media.zig Integrates artwork outcomes with media polling while preventing artwork from crossing session boundaries and retaining cache pins across transient failures.
host/src/art_cache.zig Implements bounded hash-deduplicated artwork storage with atomic publication, temporary-file cleanup, pinning, and LRU pruning.
runtime/src/image_paths.zig Adds canonicalization and containment validation for widget assets and host-owned cache images.
runtime/src/main.zig Connects validated image paths and dynamic source updates to runtime image registration.
sdk/src/reconciler.ts Updates image reconciliation so dynamic sources re-register after mutating render turns while unchanged static trees remain idle.
examples/now-playing/widget.tsx Expands the Now Playing layout and renders optional album artwork with a placeholder fallback.

Sequence Diagram

sequenceDiagram
    participant SMTC as Windows SMTC
    participant Host as Media Provider
    participant Cache as Artwork Cache
    participant Runtime as Widget Runtime
    participant Widget as Now Playing Widget
    SMTC->>Host: Session or properties event
    Host->>Host: Coalesce event into 1 Hz poll
    Host->>Host: Read and normalize thumbnail
    alt Artwork available
        Host->>Cache: Hash and atomically publish
        Cache-->>Host: Complete contained art path
    else Confirmed unavailable
        Host->>Host: Resolve frame without stale artwork
    end
    Host->>Runtime: Media frame with optional artPath
    Runtime->>Runtime: Canonicalize and validate path
    Runtime->>Widget: Register image and render frame
Loading

Reviews (6): Last reviewed commit: "fix: bound unresolved artwork replacemen..." | Re-trigger Greptile

Comment thread host/src/media.zig Outdated

@coderabbitai coderabbitai 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.

Actionable comments posted: 8

🧹 Nitpick comments (8)
host/src/media.zig (1)

100-102: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Zero-initialize artwork before handing it to the C boundary.

weaver_media_poll returns -1 on its null-argument check before the memset, so the unconditional defer weaver_media_artwork_release(&artwork) would delete[] a garbage pointer. That path is unreachable today (session/output/artwork are all non-null here), but the safety of an undefined struct crossing into C depends entirely on the callee's internal ordering. One line removes the coupling.

🛡️ Proposed change
-        var artwork: native.WeaverMediaArtwork = undefined;
+        var artwork: native.WeaverMediaArtwork = std.mem.zeroes(native.WeaverMediaArtwork);
         const result = native.weaver_media_poll(self.session, &source, &artwork);
         defer native.weaver_media_artwork_release(&artwork);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@host/src/media.zig` around lines 100 - 102, Zero-initialize the artwork
variable in the media polling flow before passing it to
native.weaver_media_poll, replacing the undefined initialization while
preserving the existing release defer and result handling.
host/src/art_cache.zig (1)

128-150: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Hoist the directory open/iterate out of the prune loop.

Each deletion reopens the root and re-statFiles every entry, so overshoot by k files costs k full directory scans. Collecting (name, mtime) once and deleting the oldest non-pinned entries keeps this to a single scan, which matches the low-CPU goal for a 1 Hz poll path.

As per coding guidelines, "Optimize for very low memory and CPU usage."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@host/src/art_cache.zig` around lines 128 - 150, Refactor Cache.prune so it
opens and iterates the root directory only once, collecting each eligible
non-pinned image filename with its modification time. Sort or select these
entries by oldest mtime, then delete only the required excess files without
repeating statFile calls or directory scans; preserve the
ArtCachePinExceedsLimit error when insufficient deletable files exist.

Source: Coding guidelines

runtime/src/image_paths.zig (2)

125-149: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

The resolver test skips everywhere except Windows despite being platform-neutral.

Directory creation, realPathFileAlloc, and containment all work on POSIX; only the case-insensitive comparison differs. Dropping the skip would give the canonical-root acceptance path coverage on non-Windows CI too.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@runtime/src/image_paths.zig` around lines 125 - 149, Remove the Windows-only
skip from the test “resolver accepts only a real file under the canonical host
art root” so it runs on all supported platforms. Preserve the existing setup and
assertions, including the platform-specific path comparison behavior handled by
Resolver.

107-123: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

windowsLexicallyContained is only exercised by its own test, not by resolve.

Production containment goes through validAbsoluteArtPath + hasParentComponent + componentContained inside resolve, so the traversal/UNC/device-namespace guarantees this test asserts are proven for a helper that never runs. Either route resolve's host-art branch through this helper, or drop it and assert the same cases against Resolver.resolve.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@runtime/src/image_paths.zig` around lines 107 - 123, Ensure the production
resolve path exercises the Windows containment logic tested by
windowsLexicallyContained. Route resolve’s host-art branch through
windowsLexicallyContained, or remove that helper and add equivalent traversal,
UNC, and device-namespace assertions using Resolver.resolve while preserving
existing containment behavior.
runtime/src/main.zig (3)

289-289: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

drainProviderFrames takes an ignored *Effects.

All three call sites were updated to pass effects, but the body discards it. Either use it (e.g. syncTimers after a drain) or revert the signature.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@runtime/src/main.zig` at line 289, Update drainProviderFrames to either use
the passed *Effects value, such as synchronizing timers after draining frames,
or remove the parameter and adjust all three call sites accordingly. Avoid
retaining an ignored effects argument.

807-818: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

seedImageStates makes a startup resolve failure fatal for the widget.

Any error from resolver.resolve on a bundled asset (unexpected symlink layout, canonicalization quirk) propagates out of main and the widget never starts, even though loadLocalImages already read the bytes successfully and synchronizeImages handles the same errors gracefully at runtime. Logging and skipping the seed for that node would keep startup resilient.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@runtime/src/main.zig` around lines 807 - 818, Update seedImageStates to
handle resolver.resolve failures per image instead of propagating them: log the
error, skip seeding that node, and continue processing remaining images.
Preserve successful state initialization and image_tree_generation updates,
matching the resilient error handling used by synchronizeImages.

851-859: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Small path strings on std.heap.page_allocator cost a full page each.

rememberRawSource and the resolved paths held in ImageState.observed are a few hundred bytes but each takes a page-granular mapping (up to 16 live, re-allocated on every track change). A small fixed inline buffer ([max_art_path_bytes]u8 is already the contract bound) or a shared arena would remove both the footprint and the map/unmap churn on the 1 Hz media path.

As per coding guidelines, "Optimize for very low memory and CPU usage."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@runtime/src/main.zig` around lines 851 - 859, Update ImageState path storage
and the replaceObserved/rememberRawSource flow to avoid page_allocator
allocations for short paths. Use the existing max_art_path_bytes contract with
inline fixed-capacity storage, or a shared arena, while preserving replacement,
null termination, and observed_valid behavior across track changes.

Source: Coding guidelines

host/src/windows_host.zig (1)

617-620: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Art-cache setup is fatal on both sides of the wiring. Artwork is an optional enhancement, but a filesystem or permission problem with the cache root aborts process startup in both the host and the widget runtime, taking down CPU/memory/audio widgets and non-artwork rendering with it.

  • host/src/windows_host.zig#L617-L620: replace try art_cache.Cache.init(...) with a catch that logs, leaves media_provider.cache unset, and skips exporting WEAVER_ART_CACHE.
  • runtime/src/main.zig#L1024-L1030: when the failure comes from the WEAVER_ART_CACHE root (error.InvalidArtCacheRoot, canonicalization errors), fall back to constructing the resolver with a null art root instead of returning from main; keep failures on the widget directory itself fatal.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@host/src/windows_host.zig` around lines 617 - 620, The art-cache
initialization failure must no longer abort startup. In
host/src/windows_host.zig lines 617-620, update the Cache.init call to catch and
log errors, leave media_provider.cache unset, and skip exporting
WEAVER_ART_CACHE; in runtime/src/main.zig lines 1024-1030, treat
WEAVER_ART_CACHE root errors such as error.InvalidArtCacheRoot and
canonicalization failures by constructing the resolver with a null art root,
while keeping widget-directory failures fatal.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/media-run-status.md`:
- Around line 66-72: Reconcile the Layer 02 status between the recorded gate
results and the “Next executable task” section: state whether the complete
layer-02 gate has passed and only F9 commit/push remains, or name the specific
outstanding gate. Update the task wording to match that status while preserving
the subsequent layer-03 restack and remediation steps.

In `@host/src/art_cache.zig`:
- Line 110: Update the publish flow around self.prune() so pruning errors are
treated as non-fatal after the file is durable, renamed, and pinned. Preserve
the successful publication and return success to applyArtwork in media.zig,
while retaining the existing prune behavior for best-effort housekeeping.
- Around line 63-68: Update the hash formatting in the digest-to-filename flow
around digest and hash to pass the SHA-256 bytes as a slice through
std.fmt.fmtSliceHexLower, ensuring hash receives the lowercase hexadecimal
representation before constructing the .img filename.

In `@host/src/windows_providers.cpp`:
- Around line 368-371: Update host/src/windows_providers.cpp lines 368-371 in
read_thumbnail to set too_large and changed, then return true for oversized
artwork so it is treated as a definite unavailable-artwork result; update
host/src/media.zig lines 148-153 in the media polling flow to handle too_large
before the changed == 0 or refresh_failed guard, preserving the warning and
artwork-retention path.
- Around line 366-378: Bound the asynchronous waits in the artwork-loading flow
around OpenReadAsync and LoadAsync so neither .get() can block the host poll
thread indefinitely. Apply a finite timeout and route timeout or
failed-operation results through the existing refresh_failed path, preserving
prior artwork and retry behavior; keep the current size checks and successful
loading behavior unchanged.
- Around line 286-301: Update the callback setup associated with
WeaverMediaSession so CurrentSessionChanged and MediaPropertiesChanged do not
capture a raw WeaverMediaSession pointer. Capture shared ownership or a
std::weak_ptr, lock it before handling each event, and return when the session
is already destroyed; ensure destruction and handler revocation cannot leave
late callbacks accessing freed state.

In `@runtime/src/image_paths.zig`:
- Around line 24-30: Change the resolved path types in the image path handling,
including Resolved.path, widget_root, and art_root, from [:0]u8 to []u8 to match
the return types of realPathFileAlloc and realPathFileAbsoluteAlloc. Preserve
the existing allocation and allocator.free cleanup behavior.

In `@runtime/src/main.zig`:
- Around line 897-914: Track transient reload failures separately from
successful observations in the image reload state. Update both read and register
failure paths near effects.registerImageBytes to mark the path as failed rather
than calling replaceObserved with a valid observation, and gate the
observedEquals fast path in the reload function on !state.load_failed. Clear the
failure state after a successful load, with bounded retry behavior for
permanently unreadable paths.

---

Nitpick comments:
In `@host/src/art_cache.zig`:
- Around line 128-150: Refactor Cache.prune so it opens and iterates the root
directory only once, collecting each eligible non-pinned image filename with its
modification time. Sort or select these entries by oldest mtime, then delete
only the required excess files without repeating statFile calls or directory
scans; preserve the ArtCachePinExceedsLimit error when insufficient deletable
files exist.

In `@host/src/media.zig`:
- Around line 100-102: Zero-initialize the artwork variable in the media polling
flow before passing it to native.weaver_media_poll, replacing the undefined
initialization while preserving the existing release defer and result handling.

In `@host/src/windows_host.zig`:
- Around line 617-620: The art-cache initialization failure must no longer abort
startup. In host/src/windows_host.zig lines 617-620, update the Cache.init call
to catch and log errors, leave media_provider.cache unset, and skip exporting
WEAVER_ART_CACHE; in runtime/src/main.zig lines 1024-1030, treat
WEAVER_ART_CACHE root errors such as error.InvalidArtCacheRoot and
canonicalization failures by constructing the resolver with a null art root,
while keeping widget-directory failures fatal.

In `@runtime/src/image_paths.zig`:
- Around line 125-149: Remove the Windows-only skip from the test “resolver
accepts only a real file under the canonical host art root” so it runs on all
supported platforms. Preserve the existing setup and assertions, including the
platform-specific path comparison behavior handled by Resolver.
- Around line 107-123: Ensure the production resolve path exercises the Windows
containment logic tested by windowsLexicallyContained. Route resolve’s host-art
branch through windowsLexicallyContained, or remove that helper and add
equivalent traversal, UNC, and device-namespace assertions using
Resolver.resolve while preserving existing containment behavior.

In `@runtime/src/main.zig`:
- Line 289: Update drainProviderFrames to either use the passed *Effects value,
such as synchronizing timers after draining frames, or remove the parameter and
adjust all three call sites accordingly. Avoid retaining an ignored effects
argument.
- Around line 807-818: Update seedImageStates to handle resolver.resolve
failures per image instead of propagating them: log the error, skip seeding that
node, and continue processing remaining images. Preserve successful state
initialization and image_tree_generation updates, matching the resilient error
handling used by synchronizeImages.
- Around line 851-859: Update ImageState path storage and the
replaceObserved/rememberRawSource flow to avoid page_allocator allocations for
short paths. Use the existing max_art_path_bytes contract with inline
fixed-capacity storage, or a shared arena, while preserving replacement, null
termination, and observed_valid behavior across track changes.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: ec87d9d1-3e11-489e-8c0e-251262654ae3

📥 Commits

Reviewing files that changed from the base of the PR and between 4dcf19c and f6a6442.

⛔ Files ignored due to path filters (3)
  • docs/media-evidence/pr02-now-playing-paused.png is excluded by !**/*.png
  • docs/media-evidence/pr02-now-playing-track-change.png is excluded by !**/*.png
  • docs/media-evidence/pr02-now-playing.png is excluded by !**/*.png
📒 Files selected for processing (16)
  • docs/media-evidence/pr02-visual.md
  • docs/media-run-status.md
  • examples/now-playing/widget.tsx
  • host/build.zig
  • host/src/art_cache.zig
  • host/src/media.zig
  • host/src/windows_host.zig
  • host/src/windows_providers.cpp
  • host/src/windows_providers.h
  • runtime/src/image_paths.zig
  • runtime/src/main.zig
  • runtime/src/tree.zig
  • sdk/CONTRACT.md
  • sdk/index.d.ts
  • sdk/src/reconciler.ts
  • sdk/test/reconciler.test.mjs

Comment thread docs/media-run-status.md
Comment on lines +66 to +72
The 15-finding adversarial remediation is in progress bottom-up. F9 is fixed
and tested in layer 02; its complete per-layer gate and publication are next.

## Next executable task

Create `media/02-album-art` from layer 01 and trace SMTC thumbnail ownership,
host cache lifecycle, runtime path containment, and dynamic image
re-registration before editing.
Run the complete layer-02 gate, commit/push F9, then restack layer 03 and fix
F1/F2/F3/F4/F10/F11/F12 at the transport-owning layer.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Reconcile the Layer 02 gate status.

Lines 36-55 record the Layer 02 gates as passing, but lines 66-72 still say the complete gate is pending. Clarify whether only F9 commit/push remains, or identify the specific gate still outstanding.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/media-run-status.md` around lines 66 - 72, Reconcile the Layer 02 status
between the recorded gate results and the “Next executable task” section: state
whether the complete layer-02 gate has passed and only F9 commit/push remains,
or name the specific outstanding gate. Update the task wording to match that
status while preserving the subsequent layer-03 restack and remediation steps.

Comment thread host/src/art_cache.zig
Comment on lines +63 to +68
var digest: [std.crypto.hash.sha2.Sha256.digest_length]u8 = undefined;
std.crypto.hash.sha2.Sha256.hash(bytes, &digest, .{});
var hash: [hash_hex_bytes]u8 = undefined;
_ = try std.fmt.bufPrint(&hash, "{x}", .{digest});
var filename_buffer: [hash_hex_bytes + ".img".len]u8 = undefined;
const filename = try std.fmt.bufPrint(&filename_buffer, "{s}.img", .{hash});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

Zig std.fmt {x} format specifier byte array vs slice hex formatting 0.15

💡 Result:

In Zig, including version 0.15, the {x} and {X} format specifiers are intended strictly for numeric types (such as integers), where they output the numeric value in hexadecimal notation [1][2][3]. Using {x} or {X} with slices of bytes (e.g., []u8) is not supported and will result in a compile-time error [4][5]. Specifically, the standard library includes checks that will explicitly trigger a compile error if you attempt to use these specifiers with types that are not integers, advising you to use alternative formatting functions [4][5]. To format a byte slice or byte array as a hexadecimal string, you must use the following dedicated functions provided in std.fmt: 1. std.fmt.fmtSliceHexLower: Formats bytes as lowercase hexadecimal [1][2][6]. 2. std.fmt.fmtSliceHexUpper: Formats bytes as uppercase hexadecimal [1][2][6]. Example usage: const std = @import("std"); test "hex formatting" { const data = "Zig!"; // Incorrect: try std.debug.print("{x}",.{data}); // Causes compile error // Correct: try std.debug.print("{}\n",.{std.fmt.fmtSliceHexLower(data)}); } This separation was implemented to avoid ambiguity, as the same specifier previously attempted to handle two fundamentally different types of data (single integers vs. collections of bytes), which led to confusion and design limitations [4]. Using the explicit function wrappers ensures clear, type-safe formatting [4].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== Zig-related files ==\n'
git ls-files | rg '(^|/)(build\.zig|build\.zig\.zon|zig\.version|\.zig-version|host/src/art_cache\.zig)$' || true

printf '\n== build.zig excerpt ==\n'
if [ -f build.zig ]; then
  cat -n build.zig | sed -n '1,220p'
fi

printf '\n== build.zig.zon excerpt ==\n'
if [ -f build.zig.zon ]; then
  cat -n build.zig.zon | sed -n '1,220p'
fi

printf '\n== host/src/art_cache.zig excerpt ==\n'
cat -n host/src/art_cache.zig | sed -n '55,80p'

printf '\n== std.fmt references in repo ==\n'
rg -n 'fmtSliceHexLower|fmtSliceHexUpper|\\{x\\}' -g '!**/dist/**' -g '!**/build/**' -g '!**/node_modules/**' .

Repository: SunkenInTime/weaver

Length of output: 1930


🌐 Web query:

Zig std.fmt "{x}" fixed-size byte array [32]u8 compile error fmtSliceHexLower

💡 Result:

The compile error occurs because the {x} and {X} format specifiers are no longer supported for slices or byte arrays in Zig [1][2]. Previously, these specifiers could be used to print bytes as hexadecimal, but this behavior was deprecated and removed to avoid ambiguity and provide clearer formatting options [1][2]. To print a byte array or slice as hexadecimal, you must explicitly use the std.fmt.fmtSliceHexLower or std.fmt.fmtSliceHexUpper formatter [1][2]. Here is the correct way to print a fixed-size byte array: const std = @import("std"); test "print hex" { const bytes: u8 = []u8{0xAB, 0xCD, 0xEF} ++ ([]u8{0} 29); // Use {} as the format specifier and pass the hex formatter as the argument std.debug.print("{}\n",.{std.fmt.fmtSliceHexLower(&bytes)}); } By passing std.fmt.fmtSliceHexLower(&bytes) into the format string with {}, you ensure the byte slice is correctly interpreted and formatted as a lowercase hexadecimal string [3][4][2]. Note that if you are passing a fixed-size array, you should take a slice of it (e.g., &array or array[0..]) to pass to the formatter [4][2].

Citations:


🌐 Web query:

Zig std.fmt formatting arrays of u8 with {x} versus fmtSliceHexLower

💡 Result:

In modern versions of Zig, the use of the {x} and {X} format specifiers to format slices or arrays of u8 has been deprecated and replaced with the explicit use of std.fmt.fmtSliceHexLower and std.fmt.fmtSliceHexUpper [1][2][3]. If you attempt to use {x} or {X} directly on a slice of u8 (e.g., std.debug.print("{x}",.{my_slice})), the compiler will trigger a compile error, typically advising you to use the corresponding fmtSliceHex function instead [1][2]. The recommended approach to format a u8 slice or array as a hexadecimal string is as follows [4][2]: std.fmt.format(writer, "{s}",.{std.fmt.fmtSliceHexLower(my_slice)}); Key points: - Deprecation: The {x} and {X} specifiers for u8 slices were special cases in earlier versions of Zig's standard library [3]. They were removed to simplify the formatting system and reduce ambiguity, as {x} for integers and {x} for byte arrays performed fundamentally different operations [1][3]. - Explicit Formatting: Using std.fmt.fmtSliceHexLower or std.fmt.fmtSliceHexUpper makes the intent explicit and avoids the issues associated with overloaded format specifiers [1][3]. - Behavior: These functions return a Formatter object that handles the conversion of each byte in the slice into two hexadecimal characters, which is then correctly processed by the formatting system [2].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== host/build.zig ==\n'
cat -n host/build.zig | sed -n '1,220p'

printf '\n== host/build.zig.zon ==\n'
cat -n host/build.zig.zon | sed -n '1,220p'

Repository: SunkenInTime/weaver

Length of output: 9326


Use fmtSliceHexLower for the SHA-256 bytes
{x} doesn’t format a [32]u8 here; use std.fmt.fmtSliceHexLower(digest[0..]) (or another slice) when filling hash.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@host/src/art_cache.zig` around lines 63 - 68, Update the hash formatting in
the digest-to-filename flow around digest and hash to pass the SHA-256 bytes as
a slice through std.fmt.fmtSliceHexLower, ensuring hash receives the lowercase
hexadecimal representation before constructing the .img filename.

Comment thread host/src/art_cache.zig
}
self.published_hash = hash;
self.published = true;
try self.prune();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

A prune failure discards a publication that already succeeded.

By Line 110 the file is durable, renamed, and pinned. If prune returns an error (e.g. error.ArtCachePinExceedsLimit when every non-pinned statFile fails, or a delete error), publish propagates it and applyArtwork in host/src/media.zig logs "retaining prior art" and drops the fresh path — even though the artwork is on disk and pinned. Pruning is housekeeping; let it fail without invalidating the publication.

🛠️ Proposed fix
         self.published_hash = hash;
         self.published = true;
-        try self.prune();
+        self.prune() catch |err| std.log.warn("art cache prune failed: {s}", .{`@errorName`(err)});
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
try self.prune();
self.prune() catch |err| std.log.warn("art cache prune failed: {s}", .{`@errorName`(err)});
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@host/src/art_cache.zig` at line 110, Update the publish flow around
self.prune() so pruning errors are treated as non-fatal after the file is
durable, renamed, and pinned. Preserve the successful publication and return
success to applyArtwork in media.zig, while retaining the existing prune
behavior for best-effort housekeeping.

Comment on lines 286 to 301
~WeaverMediaSession() {
shutting_down.store(true, std::memory_order_release);
try {
if (properties_subscribed && current) current.MediaPropertiesChanged(properties_token);
} catch (...) {
}
try {
if (manager_subscribed && manager) manager.CurrentSessionChanged(manager_token);
} catch (...) {
}
properties_subscribed = false;
manager_subscribed = false;
current = nullptr;
manager = nullptr;
if (apartment_initialized) winrt::uninit_apartment();
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🌐 Web query:

WinRT event revoke token in-flight handler completion guarantee

💡 Result:

In the Windows Runtime (WinRT), there is no guarantee that an in-flight event handler will cease execution immediately upon calling the revoke function with an EventRegistrationToken [1][2][3]. When you revoke a delegate, the event source removes that delegate from its invocation list [1][4][5]. However, if an event was already in the process of being raised (i.e., it is "in-flight"), the event source may have already acquired a copy of the invocation list or begun iterating through it [6][3]. Consequently, an in-flight event handler can still be invoked even after the revocation call has successfully returned [6][3]. Key considerations regarding this behavior include: 1. Asynchronous Events: This race condition is particularly relevant for asynchronous events [1][2]. If you revoke an event handler within a destructor, an in-flight event might reach your object after the destruction process has begun, potentially leading to crashes or access violations [1][2]. 2. Synchronous Events: For events raised synchronously, revoking the handler generally ensures that you will not receive further events from that point forward [1][2]. 3. Safety Patterns: To handle these scenarios safely, especially in C++/WinRT, it is recommended to use weak references (such as winrt::event_revoker) [1][2][7]. These mechanisms help manage the lifetime of the event recipient and provide a safer way to revoke handlers automatically as objects go out of scope [1][2]. 4. Redundancy: If an event handler is invoked after it has been unregistered, the call is considered redundant but harmless [3]. The event source is typically designed to handle these cases gracefully [3]. Developers should not rely on revocation to immediately terminate a handler that has already been dispatched by the event source [6][3]. If your logic requires ensuring that a handler does not execute, you must implement your own guard, such as a boolean flag or a weak reference check, within the handler itself [1][2][3].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the relevant file around the destructor and callback setup.
file='host/src/windows_providers.cpp'
wc -l "$file"
sed -n '220,460p' "$file" | cat -n

# Locate the destroy path and any raw-pointer captures in this file.
rg -n "delete session|CurrentSessionChanged|MediaPropertiesChanged|shutting_down|mark_session|WeaverMediaSession" "$file"

Repository: SunkenInTime/weaver

Length of output: 13004


Handler revocation doesn’t fence in-flight callbacks

CurrentSessionChanged / MediaPropertiesChanged can still be executing after revocation, and both handlers capture a raw WeaverMediaSession*. That leaves a use-after-free window when weaver_media_destroy deletes the session; capture shared ownership or a std::weak_ptr in the callbacks so late events can’t touch freed memory.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@host/src/windows_providers.cpp` around lines 286 - 301, Update the callback
setup associated with WeaverMediaSession so CurrentSessionChanged and
MediaPropertiesChanged do not capture a raw WeaverMediaSession pointer. Capture
shared ownership or a std::weak_ptr, lock it before handling each event, and
return when the session is already destroyed; ensure destruction and handler
revocation cannot leave late callbacks accessing freed state.

Comment on lines +366 to +378
const auto stream = reference.OpenReadAsync().get();
const uint64_t size = stream.Size();
if (size > max_artwork_bytes) {
artwork->too_large = 1;
return false;
}
if (size == 0) {
artwork->changed = 1;
return true;
}
const auto input = stream.GetInputStreamAt(0);
winrt::Windows::Storage::Streams::DataReader reader(input);
const uint32_t loaded = reader.LoadAsync(static_cast<uint32_t>(size)).get();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

Unbounded blocking on the host poll thread.

OpenReadAsync().get() and LoadAsync(...).get() block the caller with no timeout, and this runs inline in the 1 Hz weaver_media_poll on the host loop thread. A media app whose thumbnail stream stalls (a remote/album-art fetch behind the SMTC provider is not unusual) hangs the whole daemon loop, not just the media provider — every widget stops updating.

Bounding the wait keeps a misbehaving source contained; a timeout maps naturally onto the existing refresh_failed path, which already retains prior art and retries on the next property change.

⏱️ Sketch
-        const auto stream = reference.OpenReadAsync().get();
+        auto open = reference.OpenReadAsync();
+        if (open.wait_for(std::chrono::seconds(2)) != winrt::Windows::Foundation::AsyncStatus::Completed) {
+            return false;
+        }
+        const auto stream = open.GetResults();
C++/WinRT IAsyncOperation wait_for AsyncStatus GetResults timeout pattern
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@host/src/windows_providers.cpp` around lines 366 - 378, Bound the
asynchronous waits in the artwork-loading flow around OpenReadAsync and
LoadAsync so neither .get() can block the host poll thread indefinitely. Apply a
finite timeout and route timeout or failed-operation results through the
existing refresh_failed path, preserving prior artwork and retry behavior; keep
the current size checks and successful loading behavior unchanged.

Comment on lines +368 to +371
if (size > max_artwork_bytes) {
artwork->too_large = 1;
return false;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Oversized artwork is conflated with a refresh failure across the C/Zig boundary, making the too_large handling dead code. read_thumbnail reports the oversized case by returning false, which weaver_media_poll turns into state->refresh_failed = true; the Zig consumer then checks refresh_failed before too_large and returns early. Net effect: the dedicated 1 MiB warning never fires, and a permanently-oversized thumbnail leaves the session in a sticky "failed" state instead of a settled "art unavailable" one.

  • host/src/windows_providers.cpp#L368-L371: set too_large = 1 and changed = 1, then return true — oversized is a definite outcome, not a failure to observe.
  • host/src/media.zig#L148-L153: evaluate the too_large branch before the changed == 0 or refresh_failed != 0 guard so the warning and retention path is reachable regardless of the producer-side fix.

A regression test for the oversized path would have caught this; the new tests in host/src/media.zig cover refresh-failure and no-art but not too_large.

📍 Affects 2 files
  • host/src/windows_providers.cpp#L368-L371 (this comment)
  • host/src/media.zig#L148-L153
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@host/src/windows_providers.cpp` around lines 368 - 371, Update
host/src/windows_providers.cpp lines 368-371 in read_thumbnail to set too_large
and changed, then return true for oversized artwork so it is treated as a
definite unavailable-artwork result; update host/src/media.zig lines 148-153 in
the media polling flow to handle too_large before the changed == 0 or
refresh_failed guard, preserving the warning and artwork-retention path.

Comment on lines +24 to +30
const widget_root = try std.Io.Dir.cwd().realPathFileAlloc(io, widget_directory, allocator);
errdefer allocator.free(widget_root);
var art_root: ?[:0]u8 = null;
if (art_cache_environment) |root| {
if (!validAbsoluteArtPath(root)) return error.InvalidArtCacheRoot;
art_root = try std.Io.Dir.cwd().realPathFileAlloc(io, root, allocator);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Locate the Zig toolchain version pinned by the repo and any other uses of these APIs.
fd -H -i -t f '(\.zigversion|\.tool-versions|build\.zig\.zon|flake\.nix|Dockerfile.*)' -x sed -n '1,60p' {} \;
rg -n 'realPathFileAlloc|realPathFileAbsoluteAlloc' -C2

Repository: SunkenInTime/weaver

Length of output: 931


🌐 Web query:

Zig std.Io.Dir realPathFileAlloc return type sentinel

💡 Result:

The function realpathAlloc returns a slice of bytes, specifically []u8 [1][2]. It does not return a null-terminated sentinel string [2]. The returned slice contains the canonicalized path, and the caller is responsible for freeing this memory using the provided allocator [1][2]. Because it returns a standard Zig slice ([]u8), the data is not guaranteed to be null-terminated; if you require a null-terminated string, you must ensure it is handled separately or convert it to a sentinel-terminated type, though realpathAlloc itself provides a standard dynamic slice [2].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the affected source locations and nearby declarations.
sed -n '1,120p' runtime/src/image_paths.zig

printf '\n--- std API references in repo ---\n'
rg -n 'realPathFileAlloc|realPathFileAbsoluteAlloc|allocator\.free\(' runtime/src src -C2 || true

printf '\n--- Zig version pins ---\n'
git ls-files | rg '(^|/)(build\.zig\.zon|\.zigversion|\.tool-versions|Dockerfile.*|flake\.nix)$' || true
for f in $(git ls-files | rg '(^|/)(build\.zig\.zon|\.zigversion|\.tool-versions|Dockerfile.*|flake\.nix)$' || true); do
  echo "### $f"
  sed -n '1,80p' "$f"
done

Repository: SunkenInTime/weaver

Length of output: 24297


🌐 Web query:

Zig std.Io.Dir.cwd().realPathFileAlloc return type [:0]u8 realPathFileAbsoluteAlloc return type [:0]u8

💡 Result:

In the Zig standard library, the functions std.fs.Dir.realPathFileAlloc and std.fs.Dir.realPathFileAbsoluteAlloc return a slice of bytes, specifically []u8 [1][2]. The return type of these functions is not explicitly defined as a null-terminated slice ([:0]u8) in the standard library's public API for file path resolution [3][4]. Instead, they return a standard []u8 slice, meaning the caller receives an owned slice of bytes allocated via the provided allocator, which must be freed by the caller [5][1][6]. While some internal components or third-party codebases may utilize these functions [1][2], the functions are part of the std.fs.Dir interface (and accessible via std.fs.Dir.cwd) intended for resolving paths [7][5]. They resolve paths to their canonical form and return an allocated buffer containing the path as a sequence of bytes [1]. On Windows, this result is encoded as WTF-8, while on other platforms, it is an opaque sequence of bytes with no particular encoding [3][8]. If you require a null-terminated string, you would typically need to manually ensure or append a sentinel, though these specific standard library functions return standard []u8 slices [1].

Citations:


🌐 Web query:

site:github.com zig std.Io.Dir.realPathFileAlloc return type sentinel 0.16-dev

💡 Result:

In Zig 0.16-dev, the return type of std.fs.Dir.realpathAlloc is ![]u8 (an error union containing a slice of unsigned 8-bit integers) [1]. The function returns a []u8 slice which represents the canonicalized absolute path [1]. This slice is not null-terminated; it is a standard Zig slice containing the path data, and the caller is responsible for freeing the allocated memory [1]. As of the current development version, realpathAlloc is implemented by allocating a buffer and calling std.fs.Dir.realpath [1]. It does not utilize sentinel-termination in the returned slice [1].

Citations:


🌐 Web query:

zig lib/std/fs/Dir.zig realPathFileAlloc return type source

💡 Result:

In the Zig standard library (specifically within the std.fs.Dir interface), realPathFileAlloc is a function that returns an allocated slice of bytes representing the absolute canonical path [1]. The return type of realPathFileAlloc is: RealPathFileAllocError![]u8 This indicates that the function returns a []u8 (a slice of bytes) on success, or an error of type RealPathFileAllocError on failure [1]. Because the function allocates memory to hold the path, the caller is responsible for freeing the returned slice using the provided allocator [1]. Note that Zig's standard library APIs related to realpath have evolved, and the use of such functions is sometimes discouraged in favor of alternative approaches, such as working with relative paths or file descriptors, due to limitations with path lengths and portability across different operating systems [2][3]. Always check the documentation or source code for your specific version of Zig, as internal APIs are subject to change [4][5].

Citations:


Use []u8 for the resolved paths
runtime/src/image_paths.zig:24-30,50-57

realPathFileAlloc / realPathFileAbsoluteAlloc return []u8, so Resolved.path, widget_root, and art_root can’t be [:0]u8 here. Switching these to []u8 should make the assignments type-check; the existing free(...) calls can stay as-is.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@runtime/src/image_paths.zig` around lines 24 - 30, Change the resolved path
types in the image path handling, including Resolved.path, widget_root, and
art_root, from [:0]u8 to []u8 to match the return types of realPathFileAlloc and
realPathFileAbsoluteAlloc. Preserve the existing allocation and allocator.free
cleanup behavior.

Comment thread runtime/src/main.zig
Comment on lines +897 to +914
const bytes = std.Io.Dir.cwd().readFileAlloc(
model.io orelse return error.MissingIo,
resolved.path,
std.heap.page_allocator,
.limited(1024 * 1024),
) catch |err| {
replaceObserved(state, resolved.path, true);
std.log.err("image reload read failed; keeping the prior image: {s}", .{@errorName(err)});
return;
};
defer std.heap.page_allocator.free(bytes);
_ = effects.registerImageBytes(id, bytes) catch |err| {
replaceObserved(state, resolved.path, true);
std.log.err("image reload decode/register failed; keeping the prior image: {s}", .{@errorName(err)});
return;
};
replaceObserved(state, resolved.path, true);
state.registered = true;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

A transient read/register failure permanently pins that art path out.

Both failure paths call replaceObserved(state, resolved.path, true). On the next generation the same path resolves, observedEquals(state, resolved.path, true) matches at Line 893, and the function returns early — so the image is never retried even after the sharing violation / partial write clears. The artwork stays stale until the host publishes a different path (i.e. until the next track change).

Record the failure distinctly so a retry happens, ideally bounded so a permanently unreadable path isn't re-read on every generation.

🐛 Sketch: track the last failed path instead of marking it observed-valid
 const ImageState = struct {
     id: tree_mod.NodeId = 0,
     lifetime: u64 = 0,
     epoch: u64 = 0,
     observed: ?[:0]u8 = null,
     observed_valid: bool = false,
     registered: bool = false,
+    /// Set when `observed` was resolved but could not be loaded. Cleared on
+    /// a successful load so a transient failure retries exactly once more.
+    load_failed: bool = false,
 };
     ) catch |err| {
         replaceObserved(state, resolved.path, true);
+        state.load_failed = !state.load_failed;
         std.log.err("image reload read failed; keeping the prior image: {s}", .{`@errorName`(err)});
         return;
     };

and gate the Line 893 fast path on !state.load_failed.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@runtime/src/main.zig` around lines 897 - 914, Track transient reload failures
separately from successful observations in the image reload state. Update both
read and register failure paths near effects.registerImageBytes to mark the path
as failed rather than calling replaceObserved with a valid observation, and gate
the observedEquals fast path in the reload function on !state.load_failed. Clear
the failure state after a successful load, with bounded retry behavior for
permanently unreadable paths.

@SunkenInTime

Copy link
Copy Markdown
Owner Author

@greptileai review

@SunkenInTime

Copy link
Copy Markdown
Owner Author

Resolved the cross-session artwork P1 in commit 03c9056. A session change now marks the retained cache snapshot ineligible for the replacement frame; if the replacement thumbnail refresh fails, the provider returns no new frame, preserving the prior complete metadata+art frame atomically. The replacement frame is published only after its artwork publishes successfully or no-art is confirmed. The cache pin remains untouched on refresh failure, preserving F9. Added the regression test session replacement refresh failure suppresses prior art without unpinning it, and the final Windows live check viewed 250/750 ms old-frame retention followed by title+cover changing together at 1500 ms with no blank or mismatched frame. @greptileai review

Comment thread host/src/windows_providers.cpp Outdated
@SunkenInTime

Copy link
Copy Markdown
Owner Author

Resolved the follow-up indefinite-retention P1 in c7393f. The Windows provider now retries media properties/artwork while
efresh_failed is active. The retry is bounded by the existing subscription-only 1 Hz provider poll, so it creates no timer or SMTC work for transport-only/idle widgets. Success or confirmed no-art clears the failure and publishes the replacement atomically; a transient failure continues preserving the prior complete frame. Added
ative media retries a consumed transient refresh failure; the exact final layer-02 head passes npm 62/62, typecheck, runtime/host Zig tests, and all 18 example checks. @greptileai review

Comment thread host/src/windows_providers.cpp Outdated
@SunkenInTime

Copy link
Copy Markdown
Owner Author

Resolved the permanent-thumbnail P1 in �153e6c. Oversized artwork is now a definite unavailable outcome instead of a transient failure. Other refresh failures retry on the existing subscription-only 1 Hz path; after three failed polls, refreshed metadata is released explicitly without art while the prior cache path/pin remains untouched. Later subscribed polls keep retrying and can publish recovered art. This prevents both stale metadata/art pairing and indefinite retention. Cache publication failure uses the same artless-metadata outcome. Added tests for the three-attempt bound and permanent-unavailable frame state; all exact-head local gates pass. @greptileai review

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