# Episode 11 --- Muse Understands Music **Working YouTube title:**\ **I Broke My Own Feature On Camera (MIDI, Note-Level Diff)** **Thumbnail thought:**\ `one note. four notes. one bug.` **Target runtime:** \~8:30 ------------------------------------------------------------------------ ## \[0:00--0:20\] COLD OPEN **\[CAMERA --- Episode 10's closing line, on screen: "What does 'understanding music' get you?"\]** **GABRIEL:** Same six methods. A file format that isn't text, isn't symbols --- it's notes, velocity, pitch bend, twenty-one of these things at once. Let's find out what "understanding" actually means here. **\[TITLE CARD --- fast\]** > MUSE UNDERSTANDS MUSIC **\[Music enters --- literally, this time.\]** ------------------------------------------------------------------------ ## \[0:20--1:10\] FOUR NOTES, ONE FILE **\[TERMINAL\]** ``` text $ python3 -c "import mido; [print(m) for m in mido.MidiFile('phrase.mid').tracks[0]]" ``` ``` text MetaMessage('set_tempo', tempo=500000, time=0) note_on channel=0 note=60 velocity=80 time=0 # C4 note_off channel=0 note=60 velocity=0 time=480 note_on channel=0 note=64 velocity=80 time=0 # E4 note_off channel=0 note=64 velocity=0 time=480 note_on channel=0 note=67 velocity=80 time=0 # G4 note_off channel=0 note=67 velocity=0 time=480 note_on channel=0 note=72 velocity=80 time=0 # C5 note_off channel=0 note=72 velocity=0 time=480 ``` **GABRIEL VO:** C, E, G, C-up-an-octave. A real `.mid` file, built with a real MIDI library, not a mock. To Git this is 69 bytes of opaque binary. **\[beat\]** ``` text $ muse init --domain midi ``` **GABRIEL:** To Muse, it's about to be something else entirely. ------------------------------------------------------------------------ ## \[1:10--1:50\] TWENTY-ONE DIMENSIONS **\[TERMINAL\]** ``` text $ muse domain-info --json | jq '.domain_schema.dimensions | length' ``` ``` text 21 ``` ``` text $ muse domain-info --json | jq '.domain_schema.dimensions[].name' ``` ``` text "notes" "pitch_bend" "channel_pressure" "poly_pressure" "cc_modulation" "cc_volume" "cc_pan" "cc_expression" "cc_sustain" "cc_portamento" "cc_sostenuto" "cc_soft_pedal" "cc_reverb" "cc_chorus" "cc_other" "program_change" "tempo_map" "time_signatures" "key_signatures" "markers" "track_structure" ``` **GABRIEL VO:** Episode 05's code domain has five dimensions --- structure, symbols, imports, variables, metadata. Same six-method protocol, same `MuseDomainPlugin` interface. Twenty-one dimensions here, because music has twenty-one independent ways to change. **\[beat\]** Most of those are independently mergeable --- two agents editing reverb and pan don't even see each other. A couple aren't: change the tempo map and it's declared non-independent, because it affects how every other dimension reads. ------------------------------------------------------------------------ ## \[1:50--2:50\] ONE NOTE CHANGES **\[TERMINAL\]** ``` text $ muse checkout -b feat/raise-third --intent "raise the third from E to F" $ python3 -c " import mido mid = mido.MidiFile('phrase.mid') for t in mid.tracks: for m in t: if m.type in ('note_on','note_off') and m.note == 64: m.note = 65 mid.save('phrase.mid') " $ muse commit -m "Raise the third from E to F" --sign ``` **GABRIEL:** One semitone. E to F. The kind of edit a composer makes fifty times in an afternoon. Now let's look at what Muse actually recorded. ``` text $ muse read --json --manifest | jq '.structured_delta' ``` ``` json { "domain": "midi", "ops": [{ "op": "replace", "address": "phrase.mid", "old_content_id": "sha256:ed3b5e06...", "new_content_id": "sha256:e3c892f5..." }], "summary": "1 file modified" } ``` **\[beat, GABRIEL reading the output, visibly stopping\]** **GABRIEL:** That's not what I expected. `op: replace`. Whole file, opaque hash to opaque hash. That's what *Git* would show me. That's not "twenty-one independently mergeable dimensions" --- that's a binary diff with extra JSON around it. ------------------------------------------------------------------------ ## \[2:50--4:10\] FINDING IT LIVE **\[CAMERA\]** Let's not just accept that. The plugin's source says it should be better than this. **\[TERMINAL --- `muse code cat`\]** ``` text $ muse code cat "muse/plugins/midi/plugin.py::_diff_modified_file" ``` ``` python if path.lower().endswith(".mid") and repo_root is not None: base_bytes = read_object(repo_root, old_hash) target_bytes = read_object(repo_root, new_hash) if base_bytes is not None and target_bytes is not None: try: child_delta = diff_midi_notes(base_bytes, target_bytes, file_path=path) return PatchOp(op="patch", address=path, child_ops=child_delta["ops"], ...) except Exception as exc: logger.debug("MIDI deep diff failed: %s", exc) return ReplaceOp(op="replace", ...) # ← what we actually got ``` **GABRIEL VO:** There it is in the code: real note-level diffing exists, it's called `diff_midi_notes`, and it silently falls back to `ReplaceOp` the moment either blob can't be read from the object store --- logged at debug level, invisible by default. **\[beat\]** So which blob is missing? Let's just call the exact same function directly, by hand, against the exact same two hashes. ``` text $ python3 -c " from muse.core.object_store import read_object from muse.plugins.midi.midi_diff import diff_midi_notes old = read_object(root, 'sha256:ed3b5e06...') new = read_object(root, 'sha256:e3c892f5...') print(diff_midi_notes(old, new, file_path='phrase.mid')) " ``` ``` json { "domain": "midi_notes", "ops": [ {"op": "insert", "content_summary": "F4 vel=80 @beat=1.00 dur=1.00"}, {"op": "delete", "content_summary": "E4 vel=80 @beat=1.00 dur=1.00"} ], "summary": "1 note added, 1 note removed" } ``` **GABRIEL:** Works perfectly. F4 in, E4 out. Same two hashes, called directly. So the note-diff logic isn't broken --- something about *when* it gets called during `muse commit` is. **\[TERMINAL --- `muse code cat commit.py`, two line numbers highlighted\]** ``` text commit.py:532 structured_delta = plugin.diff(base_snap, snap, repo_root=root) commit.py:658 write_object_from_path(root, object_id, root / rel_path) ``` **GABRIEL VO:** Line 532 runs before line 658. Muse computes the note-level diff *before* it writes the new file's bytes into the object store. The new hash doesn't exist yet when `diff_midi_notes` goes looking for it. `read_object` returns nothing. The `except Exception` catches the failure silently, and every single MIDI commit, forever, has been falling back to the opaque replace. **\[beat\]** That's a real bug. In my own product. Found by making a demo for this exact episode. ------------------------------------------------------------------------ ## \[4:10--4:40\] WHAT HAPPENS NEXT **\[CAMERA\]** I'm not fixing it on camera --- fixing it means reordering writes inside the commit path, and I want to actually think about the blast radius of that before I touch it. Filed: ``` text $ muse hub issue read 203 ``` ``` text #203 MIDI structured_delta always falls back to file-level ReplaceOp assignee: gabriel label: bug ``` **GABRIEL VO:** The commit itself is correct --- the right bytes are stored, nothing is lost. Only the *observability* of the change is wrong. Still a real defect, still going in the tracker, still getting fixed after this season wraps, not glossed over for a smoother episode. ------------------------------------------------------------------------ ## \[4:40--6:00\] TWO BRANCHES, ONE NOTE, TWO DIFFERENT ANSWERS **\[CAMERA\]** The underlying diff logic works, and I can prove the merge engine still uses it correctly for conflict *detection*, even while commit's delta storage is broken. Watch. **\[TERMINAL\]** ``` text $ muse switch main $ muse checkout -b feat/louder-melody --intent "raise velocity on the melody notes" $ python3 -c " import mido mid = mido.MidiFile('phrase.mid') for t in mid.tracks: for m in t: if m.type == 'note_on': m.velocity = 110 mid.save('phrase.mid') " $ muse commit -m "Increase melody velocity to 110" --sign ``` **GABRIEL VO:** Second branch, off the *original* main --- before the pitch change. Same file, different attribute: this one only touches velocity. ``` text $ muse switch main $ muse merge feat/raise-third ``` ``` text status: fast_forward ``` **GABRIEL:** First merge --- trivial, only one side touched anything since main last moved. Now the second one. ``` text $ muse merge feat/louder-melody ``` ``` json { "status": "conflict", "conflicts": ["phrase.mid"] } ``` **GABRIEL VO:** A real conflict. Not staged --- this is what happens when one branch changes a note's pitch and another branch changes that same note's velocity, and both branches think they're the only one who touched it. ------------------------------------------------------------------------ ## \[6:00--7:00\] RESOLVING BY EAR, NOT BY FLAG **\[CAMERA\]** No conflict markers to read --- it's a binary format, there's no `<<<<<<< ours` you can put inside a `.mid` file. So I do what Episode 08 already established: read what each side actually wanted, by hand. **\[TERMINAL\]** ``` text $ muse conflicts --json ``` ``` json { "conflicts": [{"path": "phrase.mid", "kind": "file"}], "ours_commit": "...raise-third", "theirs_commit": "...louder-melody" } ``` **GABRIEL:** Ours wanted F4. Theirs wanted velocity 110. Neither side is wrong --- they're just two true things about the same note that nobody reconciled yet. So I write the file that's actually correct: F4, velocity 110, every note. ``` text $ python3 -c "... set velocity=110 on the F4-corrected file ..." $ muse resolve phrase.mid $ muse commit -m "Merge feat/louder-melody: F pitch + velocity 110" --sign ``` **GABRIEL VO:** `muse resolve`, not `--ours` or `--theirs`. Neither branch alone had the answer. The merged file does. ------------------------------------------------------------------------ ## \[7:00--7:40\] THE POINT, EVEN WITH THE BUG IN IT **\[CAMERA\]** Here's what I want you to sit with: the conflict *detection* worked. The merge engine correctly noticed two branches touched the same note. The problem is scoped to one function, in one file, running in the wrong order relative to one write. That's fixable. It's fixable precisely *because* Muse knows what a note is in the first place --- you can't have an ordering bug in a note-level diff for a domain that never modeled notes at all. **\[beat\]** Git wouldn't have this bug. Git also wouldn't have shown you F4 and velocity 110 as two separate, mergeable facts about the same note. You don't get the second thing without some risk of the first. ------------------------------------------------------------------------ ## \[7:40--8:15\] OUT **\[TERMINAL --- fading to a blank prompt\]** **GABRIEL VO:** Ten episodes of "here's what Muse can tell you." Next: what happens when the thing writing commits isn't a person sitting at this terminal at all. **\[CAMERA\]** Agents. Real ones, with real identities, making real commits --- several of which you've already watched happen in this very season. **\[CUT TO BLACK\]** > `musehub.ai` ------------------------------------------------------------------------ # Production Notes Episode 11 is structurally different from every episode before it: the demo doesn't just show a feature, it finds a real bug in that feature, live, and keeps going. That's a harder needle to thread than Episode 09's Harmony bug because this one is *in the exact command* (`muse commit`) every prior episode has trusted implicitly. Don't let the tone curdle into an apology --- it's a discovery, delivered with the same energy as finding gravity disagree with hotspots in Episode 10. ## Why This One, Honestly, Not Worked Around Per the season's established discipline (Episode 09's Harmony auto-apply bug), when a real defect is found while building an episode's own demo, the choice this time was to show it plainly rather than route around it with a lower-level function call dressed up as the CLI path. The one deliberate exception: the "does the underlying diff logic even work" check *does* call `diff_midi_notes` directly, on camera, but it's framed explicitly as debugging, not as what `muse commit` actually does --- the audience should never think that's the normal user path. ## The Merge Still Has To Be Real The Episode 11 conflict is not staged around the bug --- it's a genuine two-branch conflict (pitch changed on one side, velocity on the other, same note) that the merge engine correctly detects *despite* commit's structured-delta bug, because conflict detection and delta *storage* are different code paths. Verify this distinction holds on whatever `muse` build is current at record time before claiming it on camera again. ## Ticket Discipline `staging#203` was filed the moment the root cause was confirmed (read the exact two line numbers in `commit.py`, reproduced the underlying diff function working correctly in isolation), not before confirming it wasn't user error. Assigned to gabriel, deferred until after the season, matching `#90`--`#93`. ## Everything Here Is Real `phrase.mid` was built with `mido`, a real MIDI library --- not a byte-literal fake. Every commit, branch, conflict, and the bug itself were reproduced twice against the actual current build before being described on camera. Re-run `make-midi-episode11-demo.sh` at record time and re-verify the exact hashes and line numbers still match --- if `commit.py` has since been reordered, this episode's central beat needs re-shooting, not just re-narrating. ## The Seed The viewer arrives thinking: > **Okay, MIDI is a cute proof that domains aren't just "code."** They should leave thinking: > **Wait --- he just found and filed a real bug in his own tool, > mid-episode, and the fix was obvious once he saw it. What else is > in here that nobody's looked at yet?** That question is the entire back half of the season.