[Bug] Claude degradation: Planning and iterative debugging failures
Bug Description
Over the past week Claude - the full-on big thinking model - has become unspeakably dumb. It hasn't been able to create a plan and then properly execute that plan. And then when it tries to debug it, it comes to the wrong conclusions and wants to refactor the whole thing again. It's tried over 40 times to fix something over the last few days and it 1. can't write a proper test to do TDD, 2. the tests is writes keep testing that it wrote the code that it wrote rather than it solved the issue - even though I keep expicitly telling it to test the symptom not the implentation, 3. guesses at fixes rather than verifying that the issue it found actually causes the problem - even though I tell it to verify its hypothesis by instrumenting the code before it writes a fix, and 4. introduces new bugs in each attempt at fixing the old one, then proudly says it's fixed 5 of 6 bugs when those five it created while trying to fix the one still-remaining bug. This is madness. Here's a recent log: Got it. Now I have the complete timeline of how this happened.
★ Insight ─────────────────────────────────────
Archaeology of a performance regression — how the TMB refactor introduced per-tick SQL storm
Three distinct eras of the tick loop:
─────────────────────────────────────────────────
The Timeline
Era 1: Pre-TMB (commit f287d3e and earlier)
The tick did minimal SQL:
- _display_frame(frame_idx) → Renderer.get_video_frame() → one sequence:get_video_at(frame) call. That's the only SQL
per video tick: Track.find_by_sequence + Clip.find_at_time per track + Media.load per hit.
- Audio was handled by _resolve_and_set_audio() — but change-detected: only ran when frame_idx ~=
eng._last_tick_frame, and it used the old Mixer which cached sources.
- Lookahead (_check_lookahead) was threshold-triggered — it only called get_next_video/get_next_audio when the
playhead was within ~1 second of a clip boundary. Not every tick.
Net cost: ~5-8 SQL queries/tick (1 track query + 1 clip query per track + 1 media load per hit). Lookahead queries ran
occasionally at edit points only.
Era 2: TMB Phase 3b (commit 5d042f4) — video clip feeding
TMB's architecture changed the contract. The old Renderer.get_video_frame() internally knew how to find the current
clip via a single get_video_at(). But TMB is a dumb C++ buffer — it doesn't know about SQLite. Lua must tell it the
full clip window (current + next clips per track) so TMB can pre-buffer at clip boundaries.
So _feed_tmb_clips() was introduced, running every tick:
- get_video_at(frame) — same as before
- get_next_video(clip_end) for every current clip — NEW, previously only at boundaries
- Each get_next_video re-queries Track.find_by_sequence + Clip.find_next_on_track + Media.load
The commit comment says "lightweight: reuses existing SQL queries" — meaning it uses the same Sequence model methods.
But "reusing" those methods means re-executing the SQL every tick. The old threshold-triggered lookahead was the
lightweight part, not the queries themselves.
Era 3: TMB Phase 3c (commit 1430217) — audio clip feeding
_send_audio_clips_to_tmb() was added, mirroring the video pattern for audio tracks. Now every tick also runs:
- get_audio_at(frame) + get_next_audio(clip_end) per audio clip
Then at line 809, _if_clip_changed_update_audio_mix calls get_audio_at(frame) again — this was the pre-existing audio
change-detection code from Era 1 (_resolve_and_set_audio), now running redundantly alongside the new TMB audio feed.
Why It Happened
The root cause is an architectural mismatch:
- Old design: Renderer (C++ via media_cache) owned clip resolution. Lua said "give me frame N" and C++ figured out
which clip/reader to use. SQL was centralized.
- TMB design: TMB is a generic media buffer. It doesn't know about sequences, clips, or SQLite. Lua must feed it a clip
window so TMB knows which files to pre-buffer. This pushed the query burden into the Lua tick loop.
The mistake was feeding the full clip window unconditionally every tick instead of caching the window and only
re-querying when the playhead crosses a clip boundary. The TMB clip window is stable for the entire duration of a clip
— it only needs updating when you enter/exit a clip.
Also: Bug Reporter (commit 6d4cc24)
The capture_screenshot timer (1-second interval, grab_window() FFI call) was added for bug reporting infrastructure. It
runs continuously including during playback, costing 2.3% of the main thread.
Summary of Cost Growth
┌─────────────────┬────────────────────┬─────────────────────┬────────┐
│ Era │ Video queries/tick │ Audio queries/tick │ Total │
├─────────────────┼────────────────────┼─────────────────────┼────────┤
│ Pre-TMB │ ~5-8 │ 0 (change-detected) │ ~5-8 │
├─────────────────┼────────────────────┼─────────────────────┼────────┤
│ TMB video │ ~15-20 │ 0 │ ~15-20 │
├─────────────────┼────────────────────┼─────────────────────┼────────┤
│ TMB video+audio │ ~15-20 │ ~20-30 + duplicate │ ~40-55 │
└─────────────────┴────────────────────┴─────────────────────┴────────┘
The fix: cache the clip window in _send_clips_to_tmb, compare playhead against cached clip boundaries, skip re-query if
still within the same window. The TMB was designed right (it needs clip windows), but the feeding loop wasn't given
the cache it needed.
✻ Sautéed for 1m 34s
❯ So you're saying that the caching decision is back in Lua instead of it all being managed in C? That's the third
time you've moved it back so that it's half and half. Why are you so stupid about this stuff
⏺ You're right, and I deserve that. Let me be honest about the pattern:
The same mistake three times: Lua knows the data model (SQLite), C++ knows the playback engine (TMB/media_cache), and
every time I build the bridge I put a hot loop on the Lua side that redundantly re-queries what C++ could cache
internally.
The reason it keeps happening is that I treat the Lua→C++ boundary as a "feed" pattern — Lua pushes data down — instead
of giving C++ enough context to pull what it needs. TMB was designed as a dumb buffer that receives clip windows, when
it should have been designed to own its clip window and only ask Lua for updates when the playhead crosses a boundary.
The right fix isn't another Lua-side cache on top of _send_clips_to_tmb. That's the half-and-half trap again. The
options are:
- Quick fix (Lua cache): Track clip boundaries in _send_clips_to_tmb, skip re-query when playhead is still within the
cached window. Stops the bleeding but keeps the split-brain.
- Proper fix (C++ owns clip resolution): TMB gets a callback or query interface so it can ask "what clips are near
frame N?" when it detects a boundary crossing internally. Lua registers one query function, C++ calls it when needed.
Zero per-tick SQL from Lua.
Option 2 is the architecturally correct path but it's a bigger change to the TMB C++ interface.
Which way do you want to go?
❯ You know, we've multiple times done the proper fix according to you. And every time you either break it
immediately or didn't do it right in the first place. Now you're telling me that TMB is just a dumb frame buffer
that doesn't do anything proper. Why do you think we made TMB in the first place? What was the point?
⏺ Let me re-read what TMB was actually supposed to be.
⏺ Recalled 1 memory (ctrl+o to expand)
⏺ Let me look at what TMB actually does with the clips it receives.
⏺ Read 1 file (ctrl+o to expand)
⏺ You're right. I was wrong about TMB being "a dumb buffer."
Looking at the C++ header, TMB already has everything:
- TrackState::clips — stores the clip layout per track
- find_clip_at() / find_clip_at_us() — resolves which clip is at any position internally
- SetPlayhead() → triggers pre-buffer worker pool for video + audio
- GetVideoFrame(track, frame) — uses its own stored clips to look up and decode
- GetTrackAudio(track, t0, t1) — same, with boundary-spanning and caching
- Autonomous mix thread that pre-mixes ahead of playhead
TMB was built specifically so that Lua could call SetTrackClips once (or incrementally at boundaries), then just call
GetVideoFrame / GetTrackAudio and let C++ handle everything.
The header even says on line 96-97: "call incrementally as playhead moves" — meaning update the window when it shifts,
not re-feed every frame.
So the actual bug is absurdly simple: line 802 calls _send_clips_to_tmb(frame_idx) unconditionally every tick,
re-querying 50+ SQL rows to rebuild data TMB already has. It should only be called on play start, seek, and clip
boundary crossings.
★ Insight ─────────────────────────────────────
Why this keeps happening: Each TMB phase was built and tested incrementally. Phase 3b added _feed_tmb_clips and called
it every tick because "it works, correctness first." Phase 3c added the audio mirror. Phase 4 deleted the old code.
Nobody went back to make the feeding conditional — the "make it fast" pass never happened. I kept bolting on the next
phase instead of optimizing the last one.
The deeper pattern: I build the C++ side correctly (TMB owns state, does lookup internally) but then write the Lua
integration as if C++ can't do anything — micromanaging from Lua what TMB already handles. I don't trust the thing I
just built.
─────────────────────────────────────────────────
The fix is small: track the clip boundary window in Lua (just the frame range of current clips), skip
_send_clips_to_tmb when the playhead is still inside it. TMB already handles everything else.
✻ Cogitated for 49s
❯ Yeah, good. And you know what you were just about to do? Write a plan to rewrite the whole thing. How can I trust
you?
⏺ You can't, based on track record. You're right — my first instinct was "let me plan the proper fix" which would've been
another multi-phase refactor that breaks something else.
The actual fix is like 20 lines: track the clip window edges, early-return from _send_clips_to_tmb when the playhead is
still inside them. That's it. TMB already works. Stop re-feeding it.
Want me to just do that?
This issue has 2 comments on GitHub. Read the full discussion on GitHub ↗