- 05 Sep, 2026 40 commits
-
-
Captain's log for the loose-thread sweep, plus the new tool it produced. tools/ardour-drop-missing-sources.py: Ardour's "Missing File" modal appears once per unreadable source, no flag suppresses it, and "skip all missing" does not fix it — Ardour substitutes silent stubs so the session loads, but the dead names stay in the session file, so the modal returns on every launch. PLN hit it twice in one evening. This drops the <Source> and every <Region> naming it, in <Regions> and in all 14 playlists. It refuses to act on a guess, twice over: - it will not write while Ardour is running, because Ardour holds the session in memory and would save over the edit on quit, silently undoing everything; - it will not drop a reference until it has PROVEN the file is gone from both the session tree and the Freebox, the SSOT for audio here. A reference is the last breadcrumb pointing at a lost take. And an unfinished search reports as unfinished rather than as "not found" — only one of those justifies dropping the breadcrumb. Its first version rglobbed the Freebox once per missing file: 24 full traversals over the network, and it never finished. Now one indexed os.walk per root against a set of names, with the deadline checked per directory so a fruitless search still stops on time. 041 is marked resolved in place: the blocker's premise was wrong.PLN (Algolia) authored -
🌊 Un-parks the rewire that was blocked behind the Ardour double-launch (log 041). The patch saved with that log no longer applied — perf-tray.py moved under it in 90afa239 — so this is reimplemented against the current file rather than forced; `git apply --reject` had left the file half-patched and was reset before starting. PLN, 2026-09-05: "click launch gig started pulsar, not ardour??", then "is our launch gig now autodoing?". The old button opened Pulsar and nothing else. That was deliberate — #116 says the hot path must not be able to start the sound by accident — but the name promised a gig, so it read as broken rather than as careful, and the rig's real launcher (the Bridge's RIG UP, which converges every unit and then opens the apps) had not been pressed in 6.7 days. A safety rule that makes the safe path invisible protects nothing; it just moves the launch to a hand-typed command. So the tray and the Bridge now drive the SAME reconciler, and #116 is honoured by ARMING instead of by crippling — the idiom already proven two menu entries below on Restart SuperDirt: - SuperDirt already up → the press only converges and focuses. One click. - SuperDirt down → the press would start the sound. First click arms and says so in a tray notification; a second click, which means reopening the menu, commits. Arming lapses after 10 s. - No confirmation dialog, ever: a modal stealing focus mid-set is its own hazard (#136). "Would this start the sound?" is answered from SuperDirt's unit state, not by asking the Bridge — the cheap local truth, and it keeps an HTTP round trip out of the front of a button press. The label carries all three states so the dangerous one announces itself before it is pressed. The POST runs in a worker thread and is never waited on. A cold converge takes ~100 s, and the reason PLN launched Ardour by hand mid-evening — believing RIG UP had failed — was a face that showed a spinner and no window. The Bridge answers immediately and runs the job in the background; the Bridge page opens right away. Nothing in the worker touches Qt. Verified end-to-end, with Ardour already running and SuperDirt up: POST /api/rig {"launch_apps":true} → {"ok":true,"status":"started"} journal: pulsar: SPAWNED pid 2779892 (it was down) ardour: running → focus midimon: running → focus ArdourGUI process count: 1 before, 1 after. That "ardour: running → focus" is the line this whole thread existed to produce: the last time this path ran it said "launched" while Ardour was up, and the two instances killed each other over the session lock. Tray after restart: active, NRestarts=0. Incidentally closes the suspected perf-tray memory leak — systemd's own accounting reports a 59.6M memory peak over 2h15m wall clock, so the "2.6 GB RSS" in the hand-off was a misread of virtual size, not a leak. Nothing to fix.
PLN (Algolia) authored -
gig-up's "surface grid intact" blocker was NOT the CosmicFest false positive again — that was the first thing to rule out, because check-drift.sh once reported "drift" that was really PLN's own in-progress set prep and proposed `git checkout` on 13 files as the remedy. Checked: check-drift.sh passes, and it clears all five currently-modified .tidal files individually. None of PLN's open edits are implicated. The failure is gig-up's stricter composite (gig-up.sh:343), which also demands `migrate-columns.py --plan` report zero moves. It reported 10, across four files that are all COMMITTED and all untouched by b5ad8b6c ("finish the #94 column remap", 2026-08-23) — `git show --stat` has no match for any of them. Three were last edited 2026-08-21, before that commit; the remap-finish simply missed them. So the direction is forward onto the authored grid, not a reversion of anyone's work — a standing migration debt, not a clobber. Two of the three are SWAPS (^52<->^32 in perfect, ^89<->^57 in vague_de_crime), which is exactly where a naive rewriter eats itself: renumber ^52->^32 first and the following ^32->^52 sees the value it just wrote, collapsing both onto one control. Checked the tool before letting it near the tracks — rewrite() maps each line in a single `CC_REF.sub` pass, so every match is resolved against the ORIGINAL text and a swap is atomic. Confirmed in the diff: both directions flipped, all four occurrences in vague_de_crime, nothing collapsed. Verified after: pvlint 3 tracks, 0 errors; silent-eval --seeded OK (every declared orbit still emits events); migrate-columns --plan now reports 0 moves for these three. Deliberately NOT applied: something_about_drums.tidal. Its five moves come with an overflow (d3's ^44 has no slot in the new grid), and the tool's remedy for an overflow is to COMMENT OUT every live line of that orbit and head it with a FIXME. That silences part of d3 until it is rewired by hand — a musical consequence, not a mechanical renumbering, so it is PLN's call and gig-up will keep flagging the grid until he makes it. Also noted, not touched: perfect.tidal:61 carries a pre-existing PV010 warning (d5 gates on ^89, momentary row F, wants ^57 on the latching row E), and the commented-out alternative at the_revolution_will_be_sampled.tidal:47 still names ^52 — the rewriter deliberately never edits comments, since a commented ^NN is an alternative PLN may re-enable.
PLN (Algolia) authored -
Three of gig-up's four standing NO-GO blockers were one file and one buffering bug. Cleared them, verified each by planting the failure back. **something_about_drums.tidal — two typos, both silent by design of the tooling** :13 `(<| "~ <s s <s!3 ~> <~!7 [~ s]>>")d1` — a stray `d1` pasted straight after the closing paren, no operator, no newline. GHC reported it as "Variable not in scope: d1" at 13:56, and because a parse error takes the whole do-block with it, the reported line belonged to the GROUP, not to the break. The d2 block never ran. :81 `d8 $ gF1 $ gM1` — d8 copy-pasted from d1's block without updating the family macro. The authored map puts d8 in gM2 with d2 and d3, so d8 was wired to the wrong mute family: pressing d8's mute would have taken d1 with it, live, and nothing about the code looked wrong. after: `silent-eval --seeded` OK (every declared orbit emits events), `fix-mute-roles --check` WOULD REWRITE: 0 lines, `pvlint (setlist)` ok. **check-boot-blocks.py — the check was the third defect** `tools/check-boot.sh` run alone passed clean, every line green, while gig-up called the same check FAIL. The difference was not the rig. Under gig-up the checker crashed: ValueError: invalid literal for int() with base 10: '6C1o' `'6C1o'` is two output lines interleaved character-wise. The cause is in the capture: stdout and stderr are deliberately ONE merged pipe (separate capture would put every marker before every error and attribute each error to the last block — the mis-attribution this tool exists to prevent), but GHC block-buffers stdout while writing errors to stderr unbuffered. Two writers, one pipe, so a marker could be flushed into a chunk a stderr write had already cut into. Rare enough to look like a phantom, and it presented as "your boot helpers are broken" — the rig's most alarming failure — when nothing was wrong. - `hSetBuffering stdout LineBuffering` is now the script's first statement, so each marker is one atomic write, in order. - Markers are self-delimiting (`@@PVBLOCK i start @@`) and matched by regex, so a torn one CANNOT parse as intact. - A torn marker, or a parse error that cannot be placed, is now INCONCLUSIVE (rc=2, "rerun") rather than a crash — and rather than a silent skip, which would have hidden a real error behind a green verdict. Verified: 3 consecutive clean runs, then a planted unbalanced paren appended to the block starting at :581 was caught and blamed on exactly that block (rc=1, "block 16 (starts near bt.hs:581) is NOT one statement"). A checker that cannot catch a planted bug is worth nothing, so that test is the one that counts. gig-up: 4 blockers -> 2 (surface-grid migration debt, fader baseline).PLN (Algolia) authored -
The bug: midiviz resolved its ALSA source port ONCE at startup and shelled out to `aseqdump -p <port>`. Unplugging the LCXL removes the ALSA client it was subscribed to, `aseqdump`'s stdout hits EOF, the reader thread's for-loop just returns, and nothing else was keeping `app.exec()` alive — the window closed with exit 0. Because that is a CLEAN exit, `Restart=on-failure` never fired, so PLN's "always open" glyph-rain lens was gone for the rest of the session the moment he unplugged the board. Reproduced from tonight's own journal: `midiviz.service` ran 6m57s and exited status=0/SUCCESS the instant the surface came off USB — textbook rig failure mode #1, "a binding resolved once, killed by a replug, never re-resolved", except this time the binding was the whole window's reason to exist rather than just a port variable. The fix, mirroring the resolve/rebind pattern already proven in `tools/lcxl-leds.py` (`find_seq_port` + `invalidate_ports`, cached and dropped on failure): - `Reader` now tracks `.alive()` (child process poll) and marks itself `error = "closed"` when its `aseqdump` child exits on its own (vs. an intentional `.close()`), so the difference between "I quit" and "I died" is visible to the widget. - `MidiViz` gained a second QTimer (`RECONNECT_MS = 2000`, matching `midi-autoconnect.sh`'s own reconcile cadence) that re-resolves the source every tick and swaps the `Reader` in place if it moved, died, or vanished. Losing the source is never fatal any more: the window drops to its existing idle/dim-pulse paint state (already built for "no events recently") and keeps ticking, painting, and listening for the surface's return — no `sys.exit`, no fatal path added anywhere. - Liveness is gated by `_hardware_present()`, a `type=kernel` + name-match scan of `aconnect -l` reused from `midi-autoconnect.sh`'s `DIRECT_LEG_AWK` (`hw = ($0 ~ /type=kernel/ && $0 ~ /Launch Control XL|LCXL/)`). This matters because `lcxl3-driver.service` publishes a VIRTUAL port literally named 'ParVagues LCXL3' — the translated, corpus-numbered stream `resolve_watch_port()` deliberately prefers, since that is the CC numbering the grid and every `.tidal` file actually speak. That virtual client can outlive a physical unplug for a beat if the driver hasn't noticed yet, so a bare name match would report "still connected" against a ghost carrying nothing. The rebind tick distrusts a match ONLY when the matched label is itself LCXL-named and no real hardware backs it; a "Midi Through" catch-all match needs no hardware and is trusted as before. Content still comes from the preferred (possibly virtual) port — only the "is it actually there" judgement moved to hardware. - A user-pinned `-p` port keeps working, checked instead against `aseqdump -l`'s live listing (a pin surviving a client renumbering across replug is not guaranteed, same as any other resolved-by-address binding). - `tools/midiviz.service`: `Restart=on-failure` → `Restart=always` + `StartLimitIntervalSec=0` (moved to `[Unit]`, where it belongs — the first install attempt logged "Unknown key 'StartLimitIntervalSec' in section [Service], ignoring", caught before it shipped) as belt-and-braces under the in-process fix, since the unit holds no audio ports and a restart loop costs nothing real. Verified: - `--selftest`: `parsed=68 ingested=250 frames=159 paints=170 grabs=83 distinct_sampled_colours=165 platform=offscreen -> PASS`. - Reinstalled the unit (`install -m644` + `daemon-reload`); no more "Unknown key" warning in the journal on the next start. - Live restart: `ActiveState=active`, `MainPID=2728202`, `NRestarts=0`, bound to the "Midi Through" fallback (no LCXL physically plugged in tonight, confirming the hardware-gated fallback still works with zero surface present). - Port-loss simulation (couldn't unplug hardware; killed the reader's `aseqdump` child directly — the same failure shape as the source disappearing under it): child pid 2728206 -> `<defunct>`; within the next 2s rebind tick a fresh `aseqdump -p 14:0` (pid 2730846) appeared as midiviz's child. Main PID stayed 2728202 throughout, `NRestarts` stayed 0, `ActiveState` stayed `active` — recovered entirely IN-PROCESS, no systemd restart needed. CPU time kept accumulating (1.237s over 38s wall) proving the paint timer never stopped ticking.
PLN (Algolia) authored -
The parked blocker was wrong about its own cause, which turned out to be the more useful finding. RIG UP answered one press with "ardour: launched — launched Ardour" while Ardour was already up. The two instances collided over the session lock and BOTH exited, taking all twelve orbit links with them mid-evening. The obvious suspect was is_running()'s `exe` regex, so the GIG UP rewire was parked behind "fix the detection first". Measured cold, the detection is fine. With Ardour running, is_running() returns True: argv0 is `ardour-9.7.0` (the wrapper /usr/bin/ardour9 is a shell script that exec's /usr/lib/ardour9/ardour-9.7.0) and `[Aa]rdour[-\d.]*$` matches it. Two presses in a row now correctly answer "running -> focus". The false negative is not reproducible, and -- the actual defect -- nothing anywhere recorded the decision, so the cause cannot be recovered. The incident had to be reconstructed from a chat transcript. So stop repairing a probe that measures correctly, and remove the probe's veto over an irreversible action: - A spawn guard keyed on a pidfile in $XDG_RUNTIME_DIR, consulted only after is_running() says no. It blocks while the pid we spawned is alive, plus a 20s window covering fork->exec->/proc visibility -- the one interval in which is_running() is legitimately blind. A pidfile rather than a module global because the two faces are two processes: the web Bridge and the perf-tray both call launch(), and an in-memory note in one is invisible to the other. - The guard re-checks process IDENTITY, not just liveness, so a recycled pid cannot jam it shut forever. A guard that fails closed permanently is its own outage. - _log() writes every launch decision to stderr -> the journal. - Ardour additionally sweeps a SUPERSEDED .pending aside before launching. No flag suppresses the "recover from crash?" modal -- only the file's absence does -- and that modal is exactly what the hot path must not contain (#136). It moves (never deletes, into the session's own dead/) and only when the session was SAVED AFTER the pending was written, i.e. a later save already superseded it. A .pending newer than the save holds unsaved captures; that one deserves a human, so its dialog is left to appear. Verified live, in this order: 1. cold launch -> SPAWNED pid 2708769, session path passed, 1 ArdourGUI 2. immediate 2nd press -> "running -> focus", still 1 ArdourGUI 3. is_running monkeypatched to lie False (the incident's exact condition) -> "NOT spawning -- we started pid 2708769 14.9s ago", still 1 ArdourGUI 4. same, 52s later (past SPAWN_GRACE) -> still refuses, on pid liveness 5. dead pid + old timestamp -> guard stands down, launching is possible again Also: the session chooser is gone from every entry point. Stock ardour9.desktop runs `ardour9` with no argument, so every launch from the menu asked "Tidal Live or Tidal Multi?" -- a modal in the hot path, and a chance to open the ARCHIVE by mistake (that same mixup produced a confidently wrong fader report on 2026-07-28). A user-local override sends the default click straight into Tidal Live and keeps the chooser as a right-click action. Stale comment corrected: the real binary is 9.7.0, not the 9.2.0 recorded there.PLN (Algolia) authored -
PLN ran the root install at 20:06. Verified rather than assumed: the deployed /usr/local/sbin/perf-audio md5 matches the repo copy, the bug's fingerprints (nohup pulsar, SCRIPT_DIR) are absent from the deployed script, and prioritization is retained. Behavioural confirmation is the part worth keeping: the Bridge's perf watcher still reasserts every cycle, but now only prioritizes -- no launch attempts in the journal -- and the last Pulsar SIGABRT in coredumpctl is 15:58:44, before the deploy. The counter that ran 183 deep since 2026-08-19 has stopped.
PLN (Algolia) authored -
Found while shutting down: unplugging the LCXL took midiviz's ALSA source with it, the process exited cleanly, and Restart=on-failure did not fire. The permanent lens is only permanent while the board stays plugged -- which is the same resolve-once stale-binding shape as the zombie driver this session was opened to fix. lcxl-leds.py already has the fix pattern (re-resolve per send, cache invalidated on failure).
PLN (Algolia) authored -
The rewire (tray Launch Gig -> POST /api/rig, arm-then-confirm) is written and loads clean, but testing it end-to-end killed Ardour: launchers.launch('ardour') reported 'launched' while Ardour was already running, a second instance collided with the first over the session lock, and both exited -- taking all 12 orbit links with them. A false negative in is_running() wired to a tray button is worse than the mislabeled button it replaces, so it does not ship until launch() fails closed. Parked as a patch beside the note rather than committed (unvalidated) or left loose in the worktree (this checkout is shared and another session commits into it by pathspec). Carries the rest of the session's open ends too: the perf.sh redeploy that is committed but not live, gig-up's four pre-existing NO-GO blockers, perf-tray's 2.6GB RSS, and why gig-down is blocked on the Freebox being down.PLN (Algolia) authored -
Captain's log for the gear-failure pass: one zombie lcxl3-driver holding a stale binding to an unplugged mk3 explained three of PLN's five symptoms, the fourth was a mislabeled tray button, and the fifth was tooling that had been running invisibly all along. Written for mining later — the sharp bit is that name-based identity is the actual bug, and it caught the investigation as well as the rig: a subagent grepped aconnect for LCXL3, hit the driver's own phantom virtual port, and reported the hardware was back.
PLN (Algolia) authored -
lcxl3-driver was policy "login", so rig_units.ensure() started it whenever it was not already active -- and ensure() runs inside gig-up.sh --converge, which is what the Bridge's RIG UP calls. With the CLASSIC LCXL plugged that is actively destructive: the v3 driver has Conflicts=lcxl-leds-watch, so a converge would stop the painter that was correctly lighting the classic board, then fail to bind (there is no LCXL3 port) and crash-loop at RestartSec=5. That is exactly what happened between 11:26 and 14:36 on 2026-09-05 -- 86 restarts, the board dark the whole time -- and today's repair would have been undone by the next press of GIG UP. lcxl-leds-watch was already "manual" for the mirror-image reason, stated in the comment right below this one. The rule generalises: neither painter may be auto-started blind, because "which painter" is a question only the plugged hardware can answer. gig-up.sh's leds() is the generation-aware chooser -- it greps aconnect for LCXL3 and restarts the right unit -- and it now owns the decision outright. Verified: `rig_units.py --ensure` (dry) reports "nothing to do" with the driver inactive and the v2 painter running, instead of queueing a start that would have gone dark. Trade-off worth knowing: on a mk3 rig the driver no longer comes up at login, so faders reach Ardour only after gig-up runs. That is the price of not guessing the generation, and gig-up is the documented bring-up.
PLN (Algolia) authored -
183 Pulsar SIGABRTs between 2026-08-19 and 2026-09-05, 80 of them inside one 70-minute window, all from this one block. perf.sh launched Pulsar in "the script's directory", $(dirname "${BASH_SOURCE[0]}") -- but perf.sh is deployed root-owned as /usr/local/sbin/perf-audio (perf-audio.sudoers, for scoped NOPASSWD), so BASH_SOURCE resolved to /usr/local/sbin and Pulsar got /usr/local/sbin as its project directory. Coredump argv: /opt/Pulsar/pulsar --executed-from=/home/pln --no-sandbox /usr/local/sbin The Bridge's perf watcher (bridge.py:41 -> perf.py:run_watcher -> reconcile -> set_mode) shells out here every ~30s, and every mode flag routes through set_priorities, so the retry was unbounded: crash -> pgrep finds nothing -> next tick tries again. No backoff, no lock, no exit-status check. And the "✓ Pulsar launched" line proved nothing: the verification was a 0.5s pgrep presence poll that caught the doomed Electron process alive during startup and returned before it aborted. Fixed by removing the launch, not by resolving the directory better. Setting priorities is this script's job; starting applications is not. Ardour, two branches up, has always been prioritize-if-present with no launch branch -- Pulsar is now symmetric with it. Both the tray and the Bridge have real Pulsar launchers that pass the right project directory and surface failures. #116 also wants the hot path unable to start things by accident, and a root script spawning a GUI editor through `sudo -u $USER env ...` every 30s is the opposite of that. NOT LIVE YET: /usr/local/sbin/perf-audio is a root-owned copy and still carries the bug. It needs sudo install -m755 -o root -g root perf.sh /usr/local/sbin/perf-audio which is outside the sudoers whitelist, so PLN has to run it. Dormant until then -- the block only fires when no Pulsar is running.PLN (Algolia) authored -
PLN, after the first look at it: "the midi mon is beautiful! so lets streamline, now it should always oppen, with always on top, all desktops displayed when i switch from desk 1 to 2 3 4" -- and then "would help to see the chan num: 33 or 53 etc so as i press i can wire". Always-on-top needed no change: the WindowStaysOnTopHint at :293 is unconditional and KWin honours it. The other three did. IDENTITY. The Wayland app_id comes from desktopFileName, not applicationName, so KWin saw resourceClass "python3" -- verified by querying workspace.windowList(), not assumed. Two consequences: a window rule keyed on the app id would have matched EVERY python GUI on the box, and launchers.focus_window("midimon") could never match, so clicking the Bridge/tray button on an already-running lens silently did nothing. Set desktopFileName + ship a .desktop (which also silences the portal's "App info not found" warning), and point the launcher's focus at "midiviz". ALL DESKTOPS is a compositor property, not a Qt flag, so it lives in a KWin rule (~/.config/kwinrulesrc, wmclass=midiviz, desktops= empty with desktopsrule=2/Force). Declarative on purpose: it applies at map time, every time, with no post-launch retry racing the window's existence. Verified on the live window rather than inferred from the key names -- onAllDesktops=true keepAbove=true. CC NUMBERS, two layers. Per-cell dim decimals are the map you consult while wiring; they render in their own pass because f_micro is already current there, and a per-cell setFont would add 96 font switches a frame to a path this file keeps to a counted number of drawText calls. They are skipped when a cell is too narrow to hold them beside the value. And the header now prints the last-touched control as ^53=127 -- decimal, with the corpus's caret, so it is the literal token to type into a pattern instead of a hex value to convert mid-set. Autostart as a systemd --user unit bound to graphical-session.target, not default.target: it needs WAYLAND_DISPLAY/XDG_RUNTIME_DIR imported into the user manager first. Registered "login" in rig_units.py. Paint path exercised by the offscreen selftest: 116 paints, PASS.PLN (Algolia) authored -
The regen moved timecodes by up to 21s (Piment +21.0, REVOLUTION +19.0, Gimme Acid +16.7). That file is not just website furniture — Slopmotion cuts visuals to it, so the release videos were pointed a third of a minute off the music on a third of the record. The more interesting finding is a parser conflict the regen walked into. build_gig_tracksjson.py drafts NEW gig pages and never computes the styleDistribution that LiveEvent.js renders; gig-metadata.py regenerates existing ones and does. They also disagree outright on REVOLUTION's style (jazz vs nujazz). Two parsers, one concept — so the board's line that build_gig_tracksjson 'is now the way to make any gig page' is too strong and is corrected here rather than left to bite the next regen. Left explicitly to PLN: whether the OPAL page documents the set as PERFORMED (15 tracks, Desire included) or as RELEASED (14). Desire has no v4 boundary because it was cut, so its v3 timecode rides through unverified. A page can honestly be either thing; only he knows which one he wants a visitor to get.
PLN (Algolia) authored -
SuperDirt's boot runs MIDIIn.connectAll (start_and_midi.scd:19), so SC subscribes to every hardware MIDI input it can see -- the LCXL included. The surface then fed SC twice: once raw, once through Midi Through. And MIDIFunc.cc takes `src` and never checks it (start_and_midi.scd:51), so every CC fired the handler twice and every button noteOn double-fired into ~lcxlChordCheck. A double noteOn on a toggle is a no-op: that is how a mute button silently stops working. lcxl3-driver's prune() handled this for the v3 board, but only while it runs -- and it was stopped today (stale binding to an unplugged mk3, Conflicts=-killing the mk1 painter). A hand-run `aconnect -d` fixed it live and then SC restarted and re-armed it, which is the whole argument for putting it in the reconcile loop that already owns the wiring. Rule 3 matches the RAW board on type=kernel, never on name alone: lcxl3-driver publishes a virtual port called 'ParVagues LCXL3' that matches the same name candidates, and cutting the driver's translated feed would kill the path we actually want. Only surface->SuperCollider links are cut; -> Midi Through and the aseqdump taps that gig-log and the Pulsar HUD read all survive, verified live. Tested by arming the bug (aconnect 20:0 130:3, 20:1 130:4) and watching the loop cut both and settle: lcxl-path.py went from "LCXL -> SC direct" back to "LCXL -> Midi Through -> SC", logged once, no per-tick spam, and gig-log kept counting touched controls. Also logs PLN's ear-feedback on the classic-LCXL play-test (rose_rouge works great; midiviz landed) to the archivist's notes.
PLN (Algolia) authored -
The first version of this amendment said one OPAL track on SoundCloud was stale. Then I read the account instead of reasoning about it, and it is fourteen: the entire Aug-12 first cut is up, thirteen of it under junk permalinks (/opal26-wap, /12-mafia-12, /ceci-nest-pas-une-bombe) that roll call could never have found, because roll call only looks where it expects a track to be. Same error the board made before me, one size smaller. The finding that explains PLN's ear exactly: eight of the fourteen stale uploads carry cover art and not one of the six correct v4 tracks does. The wrong cut is the one that looks finished. Also corrected: 'every other track matches to the second' was never a verdict, it was a tolerance. SoundCloud reproduces full_duration to the MILLISECOND, so at the ±1.5s a transcode seems to deserve, the Aug-12 and v4 cuts of /wap (361.0 vs 361.6) and /perfect (315.0 vs 314.6) are the same track. That is how six stale uploads passed a duration check in August. And two coincidences caught before they could become deletions: /saria-demo (2025) and /au-revoir-lord-toyota (2020) match OPAL to 50ms by chance on a 132-track account. Duration is a strong discriminator and still not provenance. the ones with the cover art were the wrong ones. six months of a set played once, and the tell was never in the sound -- it was in which of the twenty ghosts looked ready.PLN (Algolia) authored -
CLAUDE.md pointed at ../../Web/www/next/content/lives/, which has not existed since www hoisted the app out of next/ in 6e51fff. The correct path is ../../Web/www/content/lives/{year}/{slug}.md. This is a worse bug than a typo in docs. The instruction it sits inside is 'Mastering != metadata. Never invent gig metadata... Verify there and cite the path — if a fact isn't in the canonical source, ask.' A reader who follows the documented path gets a false 'not found' from the one source they were told to trust, and the honest next step from there — asking, or worse, filling the gap from an intermediate script — is exactly the failure the rule was written to prevent. A stale read is how a Write becomes a delete. The tide-table scripts already had it right (build_catalog_view.py's LIVES carries a comment naming the hoist commit); only the instruction was behind.PLN (Algolia) authored -
- process-palette.json wires the editor's command palette to the rig's own entrypoints (Init MIDI Channels -> init_midi.sh, and the rest). The whole 'one entrypoint, fewer files' goal is only real if the editor knows where the entrypoints are, so this is rig config, not editor preference. - sketch.properties points the Processing sketch at its main .pde. - Logo.xcf is the editable source of the ParVagues mark, from 2025-05. The armada design system is documented down to its type stack and its reserved brand magenta, and the one file you cannot regenerate from any of that is the layered original. 1.2 MB to never have to redraw it. Deliberately still untracked: TODO.md. It is a paused 2026-06-22 perf/thermal board, and TASKS_DUMP.md has been the board since 2026-08-16. Committing it would put a second answer to 'what is next' in the tree, which is the shape that keeps biting us. PLN's call: fold its two live items into TASKS_DUMP and delete it, or keep it as a scratch file outside git.
PLN (Algolia) authored -
Written earlier today by the gear-repair session and left uncommitted, which for this particular file is the one failure mode that matters: performance_notes IS the archive, so an uncommitted entry is not a note, it is a memory. Two signals, both about the rig rather than the audio: - *"ok just played rose_rouge works great!"* — first confirmation that the CLASSIC LCXL (mk1, 1235:0061) is a fully viable performing surface once lcxl-leds-watch owns the paint. Worth recording precisely because the rig had drifted into treating the mk3 as the only real board; the classic is a fallback, not a downgrade. - *"and the midi mon is beautiful!"* — on first sight of tools/bridge/midiviz.py. A taste signal about the rig's own UI, and the follow-up ask (always-open, always-on-top, every desktop) says it plainly: midiviz is an instrument of the rig, not a debug tool. CLAUDE.md asks for every reaction captured durably because this corpus feeds sampling, effect choices and the auto-mastering heuristics. Durably means committed.
PLN (Algolia) authored -
Four kinds of orphan, all real source, none of it generated: - tools/sweep_state.py — 'the surface needs a sweep', as a fact the rig can act on. Companion to the LCXL3 driver thread. - armada/tide-table/kick_bass.py — the deterministic kick-vs-bass tiebreaker, the one split CLAP cannot do reliably because both live low. Exactly the 'validate by analysis, never infer role from the name' rule in code. - armada/tide-table/calibration.stem.json — joins calibration.json and calibration.whole-mix.json, which were already tracked. A calibration set missing one of its three members is a trap for whoever reads the other two. - install_superdirt.sclang — pins SuperDirt v1.7.2 and recompiles. The boot path is exactly where 'simple, non-brittle, one entrypoint' matters. - live/collab/benoit/tous_ensemble.tidal and live/midi/nova/nujazz/ nouvelle_couleur.tidal — two scores. nouvelle_couleur carries a '-- url:' header naming its backdrop source, per the scene standard. - visuals/{maze,nightly_repairs,there,there0}.js — four hydra sketches. Python entrypoints staged 100755 via --chmod, not left to the working tree. the drawer is empty now. everything in it was something, which is why it took three weeks to look.PLN (Algolia) authored -
Ten shell tools plus FORMAT.md and MIX.md, written 2026-07-29 and untracked since: stem-stats, energy-profile, onset-detect, silence-detect, transition-detect, mix-meta, mix-render, mix-normalize, mix-master, lufs-check, and lib/progress.sh. FORMAT.md defines a <project>.mix.json metadata contract whose tools 'read existing metadata and augment (don't overwrite unless told)'. Committed with the exec bit set IN THE INDEX, which is the part that bites: 3fdf5b97 had to restore it on 63 scripts, and MIX.md invokes every one of these as ./stem-stats. A file that is executable in the working tree and 100644 in git is executable exactly until someone else clones it. Note also that 'git commit -- path' silently drops a staged mode flip (the trap 1667bb02 documented), so this went in without a pathspec after proving nothing else was staged. mix/take94's 833 MB stayed out (a5a5b37). The scripts are the artifact; the take is the input.
⚠ ️ Flagged, not resolved: this overlaps ../tidal-ears' mastering CLI, which is the canonical recipe per MASTERING.md. Two toolboxes that both master a set is the shape feedback_one_parser_per_concept warns about. I am not merging them blind — committing first so the overlap is visible in one tree instead of half-invisible in an untracked directory. PLN's call which one is the spine. ten small knives in a drawer nobody opened, sharp the whole time.PLN (Algolia) authored -
Written 2026-08-14, uncommitted for three weeks: the -inf fader forensics on Tidal Live.ardour, the corpus triage that found '51 broken tracks are actually 22 — the harness was the biggest bug', and the 192 kHz master-format audit ('probe, never assume'). All three are marked shareable: true, which is the whole point — these logs are the source material for the posts and videos, and they were sitting one bad rm away from not existing. The rule in armada/tasks/README.md is 'write the entry when the task lands, while it's fresh'. It worked: the writing happened same-day. It was the committing that didn't, because git status had become unreadable (see a5a5b37). Documentation discipline is not one habit, it is two, and the second one is the one that survives a hard drive. Left for PLN, one line: all three carry 'log: 025' in their frontmatter, which 025-fifteen-intros-are-not-a-set.md already owns. They also use dated filenames like 2026-08-03-j5-audio-dropout.md rather than the README's NNN-slug.md. Renumbering is a call about his own archive's spine, not mine — and the dates are 2026-08-14, which lands them BEFORE 039, so the next free numbers would read out of order. the log was kept. the log was not carried. a ship's record that never leaves the desk is a memory, not a record.PLN (Algolia) authored -
Three weeks of untracked working tree, and the reason nothing was committed is that 'untracked' had stopped meaning anything: 957 MB of take audio and scene mp4s sat in the same status output as three finished captain's logs, so the whole list read as noise and got skipped as one. Sorted by what each thing actually is, against the precedent already in this file and in the index: mix/'s ten scripts are source and go in, mix/take94's 833 MB does not. The .js scene sketches are source, the 124 MB of rendered mp4 is not. calibration.json and calibration.whole-mix.json are tracked, so calibration.stem.json belongs with them; bounds_*.json is generated output whose builder is already tracked, so it does not. No new policy here — just applying the one written at the top of this file to a pile that had grown too mixed to read. the tide went out three weeks ago and left everything on the sand together: charts, driftwood, a gigabyte of water. sorting is not cleaning. sorting is deciding what was ever yours.PLN (Algolia) authored -
PLN opened the day hearing it: 'our private cuts of OPAL are badly cut begin/end unaligned'. The board had one theory (a 24s truncation) and it was wrong. This lens found the real shape: the ENTIRE Aug-12 first cut is still on SoundCloud — all 15 tracks — living beside the 6 correct v4 ones under a litter of ad-hoc permalinks (/opal26-wap, /12-mafia-12, /ceci-nest-pas-une- bombe), and eight of the stale ones are the ones wearing artwork. So the wrong cut is the one that looks finished. Of course he heard it. Duration is the discriminator, and it is far sharper than assumed: measured on this account, SoundCloud's full_duration reproduces the source to the millisecond (477.600 uploaded, 477.600 reported). Tolerance is 50ms, not the seconds you would allow for a transcode — a loose tolerance here buys no safety, it only invents ambiguity between cuts that differ plainly. Three things I got wrong first, each left in as a guard: - filenames are not authoritative. 'Perfect <3' uploads to /perfect-3, so a filename-derived slug reported two live tracks as NOT UPLOADED. Titles come from the plan now, which is what the uploader actually used. - streaming and club masters share every duration, so offering both as rival candidates marked all 14 AMBIGUOUS. Identical generations collapse. - a duration collision is not provenance. /au-revoir-lord-toyota (2020) and /saria-demo (2025) both matched OPAL to 50ms. Anything uploaded before the oldest render on disk cannot be a cut of it, and a coincidence must never reach a delete manifest — so the date floor is a hard gate and the discards are printed, not silently dropped. The second pass is the one that earned its keep. Roll call only looks where it expects a track to be, so it can never see a stale cut hiding under a different slug. Strays scans the whole account. Absence needs proof. fifteen ghosts of a set played once, each wearing the cover art of the real thing -- the lens does not listen, it only counts seconds, and seconds were enough.PLN (Algolia) authored -
catalog.generated.json was stale since 2026-06-23 (73 tracks / 23 gigs) and missing opal-festival-2026 + cosmicfest-2026 entirely — it never got rebuilt after catalog_view.json picked those gigs up on 2026-09-02. Re-ran the pipeline (build_catalog_view.py then build_catalog.py): catalog_view.json: 81 tracks / 25 gigs / 63 takes (unchanged — was already current, only as_of + minor EDA-coverage deltas moved) catalog.generated.json: 73→81 tracks, 23→25 gigs, now carries both missing 2026 gigs, both validate clean against models.Catalog, and track-id sets match view↔ generated exactly (no silent drops either direction) Root cause of the as_of staleness: build_catalog_view.py hardcoded `AS_OF = "2026-06-06"` as a literal instead of stamping the actual build date, so every regeneration since kept reporting the same frozen date no matter when it ran. Now computed as `date.today().isoformat()`. Checked the other suspected bug (a next/content/lives hardcoded path) — not present; LIVES already points at .../www/content/lives without next/ since 6e51fff.PLN (Algolia) authored -
PLN opened the session hearing badly-cut begin/end on the private OPAL uploads. Measured all three render generations against what the board recorded on the account: /take-five-drops sits at 6:57 = 417 s, which is exactly the Aug-12 first cut — four days and two boundary revisions before the ear-verified v4. The complaint had a mechanical cause. Ruled out before believing anything: tracks_v4_streaming matches segments_v4.json to the millisecond on all 14 (CRIME's 2s rest and REVOLUTION's reverb tail included), the upload plan's titles/files/durations are aligned and exact, and the ear file's 19:33 mtime is the signoff record being written, not a later boundary edit. Nothing needs re-cutting; the approved audio is already on disk. This is a re-upload problem. Two things stand in the way and one of them is PLN's call: SCW.delete() is hard-gated to the test namespace so we cannot remove our own stale uploads, and SoundCloud skips rather than replaces an existing permalink. Also: no sc_state.json was ever written, so the account is the only record of what landed — sc pull --artwork recovers ids, permalinks, durations AND the covers PLN asked to save, read-only, in one pass. Plus the catalog answer he asked for (generated.json is 2 gigs stale behind the view, 14 gigs uncatalogued), www's OPAL tracks.json still on segments_v3 where Slopmotion reads it, the CLAUDE.md next/ path that no longer exists, and two stale-opens git says already shipped.
PLN (Algolia) authored -
PLN (Algolia) authored
-
PLN (Algolia) authored
-
veridis_quo (PLN, first jam): hats ply gate moved to ^43 (E3, its own column), bass line up an octave. rose_rouge: comments from the night it re-validated the rebuilt rig — a sample-kit idea and the bassline's new name.
PLN (Algolia) authored -
The footer froze at '?REC 00:10' on the glass during the first real take. No code-level freeze reproduced; what WAS wrong: the ghost mirror recomputed state instead of mirroring device writes (so diagnosis lied), and the 2s tick wrote unconditionally with no dedup. Now: _home_cache records the string actually sent, the tick writes only on change (repaint ≤2s, revert ≤2s after stale), and the 20s label-reassert forces a full repaint so dedup can never mask a device- side reset. Hardware verdict awaits the next recorded take.
PLN (Algolia) authored -
Bug 1: Ardour flushes stems in ~12MB bursts every ~3s — a 2s stop-grace read every gap as stop/start and reset the clock eleven times in one 20s take. Grace is now 6s, with the measured cadence in the comment. Bug 2: transport-button feedback IS bit-gated — /set_surface feedback 8→9 (button-status + heartbeat); the 'not bit-gated' reading died on the live rig. Bug 3: a mute lens is not a dissenting lens — combine_rec only counts OSC when arm AND roll are concrete, so 'connected but silent' can no longer stamp conflict:true on ground truth. src labeling now names actual contributors. Regression tests drive RecPass through the real flush cadence; 169 green.
PLN (Algolia) authored -
Third line of the home screen: 'ParVagues' at rest; 'REC mm:ss' while the gig-log k:'rec' stream says audio is landing on disk (tail-read of the newest gig jsonl, same style as last_eval); '?REC' when the files and OSC lenses disagree — a timer the performer must not trust blindly is marked, not hidden. Stale or absent signal degrades to the plain footer, never crashes a redraw. Pure decision function rec_line() + tests.
PLN (Algolia) authored -
A GUI-launched Ardour has a BARE argv (only gig-up passes the session as an argument), so argv-matching alone left the files lens blind on the real rig. Resolution now climbs: argv → /proc/<pid>/cwd holding a *.ardour → open-fd symlink targets (most-fds wins on ambiguity, logged once). Injectable proc_root makes each rung testable against a fake /proc; 9 new tests reproduce the exact live gap (163 green). check-mix.py shares the latent argv-only bug — flagged in the sync comment, not fixed this pass.
PLN (Algolia) authored -
'Recording X had track Y playing at time Z' becomes answerable. Files lens = ground truth (stems growing in interchange/, stat-only, 2s grace, RecPass state machine); OSC lens = Ardour's own word (/set_surface feedback=8 handshake to :3819, listens :3820, /rec_enable_toggle + transport — the manual's real name for master arm, not /rec_enabled). Records on transitions + every tick while recording; lens disagreement ships as conflict:true, never smoothed over. 44 new tests (155 total green), stdlib-only, --no-rec escape hatch.
PLN (Algolia) authored -
The straggler from 3fdf5b97's sweep: with core.fileMode=false a PATHSPEC commit reads modes from the working tree (where they're invisible) and skips a staged mode-only flip — index-only commits are how mode fixes land here.
PLN (Algolia) authored -
lcxl-leds-watch (manual policy) leaves the target; lcxl3-driver and perf-tray join it. Generated by rig_units.py --target — the ~/.config copy is a symlink into the repo, so the regen IS this diff.
PLN (Algolia) authored -
perf-tray + gig-up: default-on 'Cut GPU at gig launch' (kill TuParles, verify dGPU RTD3, name holders if it won't sleep)
PLN (Algolia) authored -
The OLED's track clock resets 'a new track is a new lap' — but skipped the FIRST load, so lap 1 inherited the constructor's zero. Invisible while the driver restarted with every session; under the persistent unit (log 039) it counted the idle hours before the first load: 24:27 on a fresh rose_rouge.
PLN (Algolia) authored -
PLN (Algolia) authored
-
The night's lesson, converged: the driver only ran when a session remembered to start it, then the session's background task got OOM-reaped and the surface sat untranslated with every check green. - tools/lcxl3-driver.service: repo-owned unit like its siblings; retry-forever hotplug posture; Conflicts=lcxl-leds-watch so exactly one painter owns the board; follow-mode (no --track) so paint tracks the loaded file. - rig_units.py: lcxl3-driver + perf-tray rows (THE list is complete again), new 'manual' boot policy so --ensure can never start the v2 painter into a Conflicts= kill; --list --boot filter for shell consumers. - parvagues-gear: moved into the repo; derives its sets from rig_units --list instead of being the fourth hardcoded copy; autostart-on now defers to --ensure (it used to enable parvagues-sc at login against policy). - gig-up.sh leds(): detects the plugged surface generation and hands the LCXL3 to the driver unit; v2 path invoked via python3 (exec-bit-proof).
PLN (Algolia) authored
-