1. 22 Sep, 2026 9 commits
  2. 21 Sep, 2026 4 commits
  3. 20 Sep, 2026 5 commits
  4. 18 Sep, 2026 1 commit
    • fix(gig-up): speak the dialect the tray speaks — accept --converge and --quiet · fa548f0f
      Every tray GIG UP click POSTed to the Bridge, which ran
      'gig-up.sh --converge --quiet' — flags this script never parsed, so the
      arg guard rejected it with 'unknown option' and exit 2, output swallowed
      by the Bridge's capture. Net effect: Pulsar opened (PRE_APPS, before the
      POST) and nothing else ever booted. The script and its caller had
      drifted apart on paper only — this is the contract, written down.
      PLN (Algolia) authored
  5. 17 Sep, 2026 2 commits
    • feat(tray): Bridge-styled menu, de-dashed copy, keyboard mnemonics · 6fb0f98d
      The double click was the appindicator extension's, not ours: it reserves
      double-click for SNI Activate (instant menu) and delays single-click by
      400 ms, so a single click feels dead. Nothing to fix tray-side; documented
      in the changelog.
      
      - BRIDGE_QSS restyles the Qt-drawn popup on dark desktops: tonal surfaces,
        hairline borders, hover pill, magenta on the engaged radio; circles for
        radio groups, squares for boolean toggles. Contrast measured: body 14.7:1,
        disabled 6.4:1, radio ring 3.3:1 (bumped from 2.3:1 in review).
      - stat rows and the thermal tooltip read in tabular digits.
      - every user-visible string loses its em dash (colons, parens, middots);
        same pass on gearbox.py labels (shared with the web Bridge) and the
        launcher blurbs; unknown gearbox placeholder is now n/a, not a bare dash.
      - mnemonics on the action rows (&Open Bridge, &GIG UP, &Restart SuperDirt,
        R&ig, &Start at login, Cut G&PU, &Quit); verified over DBusMenu that Qt
        exports them as underscores, so the shell-drawn twin gets them too.
        Literal ampersand does not survive the export, so the copy avoids it.
      PLN (Algolia) authored
    • fix(launcher): converge the rig units — the tray and lens died and nothing brought them back · 4ea24cb0
      The GUI launcher started SuperDirt, Ardour and Pulsar as raw processes and
      never touched the systemd --user layer, so a tray that exits 0 when the shell
      takes its host away (perf-tray, Sep 14) or a user-closed MIDI lens (exit 78)
      stayed dead: the only convergers were tools/gig-up.sh --converge, rig-doctor
      and the Bridge watcher, and the Bridge was dead too. Three days of launches
      "worked" with no tray and no lens.
      
      ensure() now refuses to start a unit whose proof process already runs outside
      it — which is what makes it safe for the launcher to call --ensure --apply
      after scsynth is up instead of spawning a second SuperDirt through
      parvagues-sc.service. --status agrees, so a launcher-started SuperDirt is no
      longer a false problem row. gig-up.sh converges right after the ready-wait,
      and honours the midiviz close-latch (the tray's MIDI Monitor button is the
      way back within a session).
      PLN (Algolia) authored
  6. 11 Sep, 2026 1 commit
    • fix(gig-up): SuperDirt starts under pw-jack, or it steals the card · 6e49f925
      open_term() launched sclang directly, bypassing parvagues-sc.service and its
      10-pipewire-jack.conf drop-in. With jack2 installed, ldconfig hands scsynth the
      real libjack, it finds no server and starts jackd on hw:sofsoundwire, which
      takes the card from PipeWire by D-Bus reservation. Meters move, nothing is
      audible (raw jackd -dalsa bypasses ALSA UCM, so the SOF speaker route is never
      enabled), and Bluetooth cannot work at all since jackd has no BT backend.
      
      Also adds tools/parvagues.desktop so the ordered launcher is one Super-key away;
      nothing had ever been installed in ~/.local/share/applications.
      PLN (Algolia) authored
  7. 07 Sep, 2026 15 commits
    • fix(tray): the menu the shell will not draw, and the checks that missed it · 440d086c
      "i see the parvagues tray icon, but doesnt react when i click" — twice, with
      the unit green both times. Three findings, in the order they mattered:
      
      - The menu WAS exported and populated all along (19 rows over
        com.canonical.dbusmenu). What made it useless is the style: with
        QT_QPA_PLATFORMTHEME=qt5ct and no qt5ct config under the unit, Qt picks
        `qt5ct-style` with a bare #efefef palette — a 2009 grey box on a Yaru
        desktop. Now: Fusion, plus an explicit dark palette when the desktop asks
        for dark (GNOME's color-scheme is the one setting that knows).
      - The tray host on GNOME is an EXTENSION, and it registers its
        StatusNotifierWatcher seconds after login. perf-tray printed "No system
        tray available", exited 1, and only Restart= saved it 3 s later. A slower
        login loses that race. Now it waits up to 120 s, polling on a QTimer
        (availability arrives over D-Bus, so the event loop has to be spinning).
      - New check `perf-tray reachable`: unit active, item REGISTERED with the live
        pid (a stale item from an old pid looks identical on screen), and the menu
        populated over dbusmenu — the only one of the three that a click needs.
        Negative-tested against a stopped tray.
      
      Also:
      - `wireplumber churn` check: starts per hour, and who asked for them. The
        existing checks could only see the wreckage (a failed unit, a dummy sink);
        this sees the cause while audio still works. It FAILs on tonight's 48.
      - The kwin midiviz-pin check asked KDE questions on a GNOME session, so it
        warned forever about a file that will never exist. Desktop-aware now: a
        check that cries wolf is one nobody reads on the day it is right.
      PLN (Algolia) authored
    • feat(monitor): headphones mode — every orbit in your ears, no Ardour · 5818b2b0
      The rig is multichannel by design: orbit k is the pair (out_{2k-1}, out_{2k}),
      wired to its own Ardour track at a gig. Away from the stage that design means
      d1 is the only orbit that can reach a sink — and on a freshly booted laptop
      (2026-09-07) not even that: scsynth had NO links to any sink, so the whole box
      was silent with every check green.
      
      - tools/fold-orbits.py: fold all orbit pairs onto one stereo sink, summed at
        unity. Idempotent, and it VERIFIES — pw-link exits 0 for "File exists" and
        for a port that vanished mid-call, so its exit status proves nothing. The
        channel count is discovered from the graph (28 today, 12 in an older
        comment, 2 on stock SuperDirt), never assumed. Refuses while Ardour runs,
        which owns the mix and prunes SC->hardware links every 2 s.
      - gig-up.sh --headphones: SuperDirt -> fold -> Pulsar. No Ardour, no fader
        restore; the fold becomes the readiness gate, because it IS the whole
        monitoring path.
      - gig-preflight: `monitor path` check — stage (Ardour receiving on N tracks)
        or headphones (fold present), and it asks fold-orbits rather than
        re-deriving the port math.
      - 18 tests, fixtures captured from the real graph: `pw-link -lo` marks links
        by INDENTATION only, so a grep of it fabricates relationships, and a source
        port printed with no indented child is the exact state this tool fixes.
      
      Measured, not assumed: 14/14 orbits arrive at the sink (bd swept d1-d14, peak
      per orbit), and the links survive 32 s of autoroute reconciling.
      PLN (Algolia) authored
    • fix(doctor): failed system units warn, failed user units fail · 0af672a5
      First production run of the failed-units check flipped the whole set-
      readiness verdict on apport-autoreport.service — honest but wrong: the
      rig runs entirely in user scope, and system-scope corpses (apport, snapd
      chores) rarely gate a set. User scope stays a FAIL.
      PLN (Algolia) authored
    • fix(audio): detect and heal the undead server — the spin that wedges the DSP · 26a526cf
      An scsynth whose pipewire-jack client loses its startup race gets no
      negotiated format (quantum 0) and busy-spins its data loop at ~95% of a
      core at RT priority, unit green, process alive. Hours of that starved
      the SOF DSP's IPC until the firmware wedged and every sink vanished.
      Same config also boots clean at 1.5% — a probabilistic race, so the
      durable answer is detection + self-heal:
      
      - sc-watchdog spin detector: /proc CPU deltas every poll (zero forks),
        after 10s sustained >=85% ONE pw-top shot decides — quantum>0 is a
        musician playing and is never touched; quantum 0 is the spin
        (restart via the shared rate limit). Suite 15/15 incl driven-guard.
      - rig-doctor: SOF DSP IPC-timeout check (with the no-root cold-boot
        cure), failed-units check (start-limit trap, negative-tested),
        scsynth orphan-spin check on the same quantum oracle. 51 checks.
      - start_and_midi.scd: mi global effects measured LIGHT (all=1.3% vs
        off=1.7% idle, 5 boots) — acquitted, kept ON, gated behind PV_MI_FX
        so the next suspicion is a knob-flip, not a guess.
      - CHANGELOG Sprint 8 + backlog closure.
      PLN (Algolia) authored
    • fix(autoroute): never prune SC->hardware links when Ardour is absent · 1289e086
      The PRUNE ran unconditionally every 2s. With Ardour closed it deleted
      SuperCollider's ONLY output links, and a linkless pipewire-jack client
      self-drives its data loop into a 100%-CPU error spin at RT priority.
      Hours of that starved the SOF DSP's IPC until the firmware wedged
      (IPC timeout -110) and every sink vanished (2026-09-07). 'SC must
      never reach hardware' is Ardour-session policy, not idle-laptop policy:
      routing and pruning now both require Tidal tracks in the graph.
      PLN (Algolia) authored
    • Pre-compact cleanup: CHANGELOG Sprint 7 + archive entry + memory · 7a2b2886
      Sprint 7 covers the morning: mi-UGens s.sync, the preload pointed at what gets
      played, three stacked bugs in its freshness check, 198 AppleDouble deletions,
      s.latency 0.3->0.2, and the wireplumber start-limit outage.
      
      Archive entry carries nine learnings, the load-bearing ones being that pgrep
      costs 53-59ms a call here while the real trick is the cheap prefilter (the regex
      on every comm is WORSE than the pgrep it replaced); that case-in-a-variable does
      no alternation; that start-limit-hit is this box's signature trap and retrying is
      the one action that cannot work; and that a valid measurement can be silent,
      because mute sits downstream of all DSP while a null sink lies.
      
      Memory: verify-the-leg-that-breaks gains the three 2026-09-07 instances plus two
      rules (ask why two numbers disagree; a tool owns the commands it prints).
      rig-state records the reset-failed lesson and the BT-by-name detail.
      PLN (Algolia) authored
    • fix(audio): wireplumber start-limit takes all audio out, and the doctor could not see it · 9edfd743
      PLN: 'media keys dont work' + 'tried bluetooth headset couldnt get sound there'.
      Cause was neither:
      
          wireplumber        failed (start-limit-hit)
          Sinks:  100. Dummy Output      <- the ONLY sink
          0 bluez objects
      
      pipewire is the graph; wireplumber is the session manager that puts DEVICES in
      it. Without it there are no device nodes at all -- every hardware sink vanishes,
      PipeWire invents Dummy Output, media keys act on nothing, and a Bluetooth
      headset cannot appear however well it pairs. It reads exactly like dead
      hardware.
      
      The trap is StartLimitBurst=5 / 5min with Restart=on-failure. Five hand-issued
      restarts inside two minutes exhausted it, and from then on restarting AGAIN
      cannot work. Cure is reset-failed, then start. Third instance of this exact
      shape in one session after parvagues-sc twice: on this box, 'X will not start'
      means check reset-failed first.
      
      rig-doctor's check_pipewire tested pipewire on PATH, pw-jack on PATH, and
      pipewire.service is-active -- and pipewire.service was ACTIVE for the whole
      outage, so the check was green while the box had no audio at all. Presence
      versus function again. It now also asserts wireplumber is active and that the
      graph holds at least one real output (alsa_output/bluez_output, never
      auto_null/Dummy), FAILs with the reset-failed cure in the fix field, and WARNs
      on a dummy or unset default. It caught a live regression the first time it ran.
      
      The headset itself was only MUTED: bluez_card active profile was already
      a2dp-sink, and the sink read '[vol: 0.20 MUTED]' -- per-device state wireplumber
      persists. Unmuted by NAME, because the BT node id moves between calls while the
      device reconnects. Not caused by the quantum/mute testing, which ran at 00:10
      with no BT device connected and left force-quantum verified at 0.
      
      latency-lens --print-config used to hand over 'systemctl --user restart pipewire
      pipewire-pulse wireplumber' with no warning, which repeated is precisely this
      trap. It now says ONCE, states the consequence, gives the cure, and points at
      the runtime setting that needs no restart.
      
      Doctor: 48 checks, 0 fail, 10 warn, 38 pass.
      PLN (Algolia) authored
    • perf(latency): s.latency 0.3 -> 0.2, and make 'late' a counted metric · 79ff7c9f
      The comment read 'increase this if you get late messages', and the rig had 136
      of them on 2026-09-06, so the arrow only ever pointed one way. Value history: 1
      second, then 0.3, never revisited.
      
      Those 136 were not a latency shortfall. Two bursts (64 at 19:58, 72 at 20:03)
      with values DECAYING 8.1s -> 0.65s: a backlog draining, not a trickle of
      near-misses. Both sit against SuperDirt restarts, and the 20:03 one is the
      watchdog's spurious restart-on-boot fixed in b9564095 -- SC restarted, its clock
      reset, Tidal kept its logical clock, the backlog flushed as 'late'. The
      documented reason to pad this was a misdiagnosis of a bug that no longer exists.
      
      0.2 is SuperCollider's own documented default and this is a direct tax on how a
      ctrl+enter feels. Lower is fair game; the floor is whatever keeps 'late' at zero
      under the densest material. Clean boot after the change: 0 lates.
      
      latency-lens now counts 'late' per candidate alongside pw-top's ERR, and a late
      rejects a quantum on its own: ERR is the audio graph missing its deadline, late
      is the MUSIC being handed over too late to play on time. Different failures, and
      a quantum decision wants both. Matched as 'late <digits>' -- an earlier hand
      grep for the bare word scored six hits on metalPlate.scd and inflated the count.
      PLN (Algolia) authored
    • fix(preload): make the freshness check trustable, and delete 198 fake wavs · 46b521d8
      Deleted 198 AppleDouble resource forks named zz._*.wav, on explicit owner
      instruction. Every one confirmed by file(1) magic before removal rather than by
      name pattern -- 198/198 candidates came back 'AppleDouble encoded Macintosh
      file', zero false positives, 0.3 MB. 69 were in playable banks
      (rhadamanthe_fx 227->189, _divers 314->293, _vocal 77->72, _melo 32->30); 129
      were under Samples/baba/__MACOSX/, whose emptied dirs were rmdir-ed. Sample
      indices were untouched: the zz. prefix sorted them after every real sample,
      which is presumably why someone renamed rather than deleted. The boot is now
      free of the 'WARNING: File reading failed' wall.
      
      Then three faults in the freshness path, each hiding the next:
      
      1. preload.scd's mismatch banner said 'whitelist is STALE, regenerate it' when
         the cause was unreadable files, and regenerating provably changed nothing.
         Now reports 'N bank(s) DID NOT FULLY LOAD', separates got==0 (whole bank) from
         0<got<expected (folder changed, or some files are not audio), and prints the
         file(1) one-liner that distinguishes them.
      
      2. check-preload.sh compared bank NAME SETS and never counts. The plan said
         rhadamanthe_vocal=77 against a folder of 72 and the checker printed 'ok' while
         the boot said 'expected 77 files, got 72' -- and --fix refused to regenerate,
         because by the name-set test nothing had changed. The everyday way to hit this
         is dropping a new pack into an existing bank: every new file lazy-loads
         mid-set under a green check. Now compares name+count pairs and prints
         'plan N -> disk M' per drifted bank.
      
      3. The bank-name regex was case-blind. [a-z0-9_]+ truncated rampleA0 to rample
         and collapsed every vocalOoh1/vocalScatJ into one 'vocal', which is why the
         checker said 111 banks while SuperDirt loaded 124. 124 was always the truth;
         the regex was wrong in both directions, undercounting the plan AND inventing
         two bank names that do not exist. bank_counts() had inherited it, so drift in
         a camelCase bank stayed invisible even after fault 2 was fixed.
      
      Three numbers now agree: checker 124, boot '124/124 banks OK in 5.2 s',
      independent parse 124/124/no gap. Drift tested both directions on a camelCase
      bank, plan byte-identical afterwards.
      PLN (Algolia) authored
    • docs: the gig records are at Perso/www, not Web/www · 580e0000
      The documented path did not exist; resolving it cost a lookup while building
      gen_setlist.py. Also points at the judge_specs ear lists, which is where a gig
      lives before its tracks.json is built -- the cosmicfest set among them.
      PLN (Algolia) authored
    • fix(preload): warm what gets played, computed from the gig records · f68277f2
      The preload warmed a setlist FILE, and the only one was armada/setlist_opal2026
      .txt -- the 16 tracks of one August gig. So the plan was fresh, correct, and
      aimed at the wrong set. Measured: driving rose_rouge logged 41 'reading
      soundfile as needed' lines in 180s, every one of them the rose bank, while ZERO
      of the 60 warmed banks lazy-loaded.
      
      That is check-preload.sh's documented failure one level up. There the plan was
      stale because two tools disagreed about the setlist; here the plan matches its
      setlist exactly and the SETLIST has drifted from what gets played -- which no
      plan-vs-setlist freshness check can see, because the two agree.
      
      gen_setlist.py computes the list instead of maintaining one, from the canonical
      records in authority order: <www>/content/lives/<year>/<slug>/tracks.json, which
      carries both a date and each track's exact repo-relative file; then
      armada/tide-table/judge_specs/*_setlist_ear.json for gigs whose tracks.json is
      not built yet -- where the cosmicfest set lives, 'THE ground truth, 14 tracks',
      with rose_rouge at #5. Ear lists carry no gig date, so they are included and
      flagged rather than dated by guesswork.
      
      60 banks -> 111, 16 tracks -> 39. 51 banks were one first-play from a disk read.
      rose is covered.
      
      check-preload.sh now computes its own list rather than borrowing
      set-coherence.setlist_tracks(), because the two ask different questions: a cheat
      sheet wants tonight's running order, a preload wants anything that might get
      played, and the cost asymmetry is total. PV_PRELOAD_SETLIST / PV_PRELOAD_MONTHS
      override. Not a fourth setlist parser -- the same setlist_samples parser, fed a
      wider list.
      
      Cost of over-covering, measured: 122/124 banks OK in 5.0s (was 60/61 in 2.6s),
      scsynth RSS 4.8G on a box with 62G total and 40G available.
      
      Logs two findings from the same boot: 64 AppleDouble resource forks named
      zz._*.wav in rhadamanthe_fx/divers/vocal, which fail to load AND occupy sample
      indices (left alone -- the zz. prefix looks like a deliberate rename, so
      deleting is the owner's call); and preload's COUNT MISMATCH banner, which
      reports those 2 real load failures with the wrong diagnosis.
      PLN (Algolia) authored
    • fix(sc): every orbit's MiVerb/MiClouds/MiRipples send was a dead node · cd90dcb0
      On every clean boot the server said, 84 lines of it:
      
          *** ERROR: SynthDef global_mi_verb2 not found
          FAILURE IN SERVER /s_new SynthDef not found
      
      3 SynthDefs x 14 orbits x 2 lines. So '# verbwet', '# cloudswet' and
      '# ripplesreson' did nothing on any orbit, for the whole session -- silently,
      because a missing global effect is an absence of effect rather than a noise you
      notice.
      
      mi-UGens was installed correctly all along (11 .so + 11 classes) and the
      SynthDefs were defined correctly in start_and_midi.scd:273-296. The bug was
      ordering: SynthDef(...).add is ASYNCHRONOUS -- it compiles locally, sends
      /d_recv and returns -- while the very next statement sends /s_new for those
      names via initNodeTree. The /s_new overtook the /d_recv. One s.sync before the
      registration, legal inside s.waitForBoot's Routine; the idiom was already
      commented out twelve lines up. 84 errors before, 0 after.
      
      spectral-freeze above deliberately does NOT need it: it replaces a def whose
      synths are built per event, long after /d_recv has landed.
      
      Monitors it as gear, since presence checks could not see any of this:
      - mi-UGens extension now enumerates the 11 plugins AND 11 classes we use and
        names whichever is missing. d.is_dir() is true whether the directory holds a
        working install or a stale README.
      - SuperDirt boot errors (new) scopes the journal to the CURRENT boot and FAILs
        on SynthDef-not-found / FAILURE IN SERVER, naming the distinct defs rather
        than one line per orbit. Generic question, so it catches the next
        async-ordering bug too. WARNs when parvagues-sc is down, which is the correct
        on-demand state before a set.
      
      Both tested in both directions: FAIL on the recorded pre-fix journal window
      naming all three defs, PASS on the post-fix boot. That test is what caught 're'
      never having been imported in rig-doctor.py -- the new code was its first user,
      so the check would have crashed the doctor the first time it found something.
      
      Doctor: 47 checks, 0 fail, 10 warn, 37 pass.
      PLN (Algolia) authored
  8. 06 Sep, 2026 3 commits
    • perf(audio): the rig was reading samples off disk while playing · 077e1db1
      Went looking for latency to shave in the PipeWire quantum. The quantum was not
      the problem.
      
      preload.scd did not exist on this box. start_and_midi.scd:259 tests for it with
      File.exists and silently takes the other branch -- 'no preload.scd, lazy-loading
      samples on demand.' Driving rose_rouge headlessly for three minutes logged 46
      'reading soundfile as needed' lines, one every 5-12s, each a disk read landing on
      the audio thread; the SuperCollider node's pw-top W/Q hit 1.300 at the stock 1024
      and 1.390 at 512, i.e. missing its deadline regardless of buffer size.
      check-preload.sh named it exactly: '60 bank(s) would be read from DISK on first
      play (crackle, mid-transition)' -- the same sentence its own header attributes to
      a crackle that debuted at a venue.
      
      Generated with check-preload.sh --fix (60 banks, 16 tracks) and verified at the
      next boot: 'PRELOAD: 60/61 banks OK in 2.6 s'. The mechanism was never broken;
      gig-up.sh already regenerates the plan and the checker already exits non-zero.
      gig-up.sh had simply never been run on this box.
      
      Re-running the identical load after the fix left 29 lazy reads, ALL of them the
      rose bank -- because rose_rouge is not in setlist_opal2026.txt. Zero of the 60
      preloaded banks lazy-loaded, so the plan works; it warms the OPAL set while the
      track this box actually plays sits outside it. Logged, with two more findings
      from the same boot: preload's own COUNT MISMATCH banner firing on a
      sixty-second-old plan, and three global mi-UGens FX synthdefs failing to load at
      every boot while rig-doctor's mi-ugens check stays green.
      
      Adds tools/latency-lens.py, which implements the documented quantum method
      (lowest quantum holding zero xrun delta and max W/Q under 0.75) against pw-top,
      sets clock.force-quantum at runtime and restores it, and never edits a config.
      It refuses to run unless the default sink is muted and re-checks that every
      second for the whole run: a valid quantum test needs the real hardware device
      driving the graph but does not need the speakers.
      PLN (Algolia) authored
    • fix(watchdog): a boot is not a death, and the rig only gets three starts · b9564095
      parvagues-sc.service is Type=simple, so systemd calls it active the instant
      sclang execs -- but scsynth is sclang's child and appears ~8s later. The loop
      acted at MISSES_TO_ACT*POLL_SECS = 6s, so every healthy start earned a restart:
      
        23:17:12 scsynth GONE (3 polls) while parvagues-sc.service is active
                 -- restarting (0 prior in window)
        23:17:21 scsynth up after 8s
      
      '0 prior in window' on a start where nothing was wrong. The restart was not the
      damage; the rate limit was. Each clean start spent two of systemd's
      StartLimitBurst=3, so a second start inside StartLimitIntervalUSec=5min hit the
      limit and left the unit failed/start-limit-hit -- refusing to start at all until
      reset-failed. On stage that is indistinguishable from a dead rig.
      
      BOOT_GRACE_SECS=30, measured from the unit's own ActiveEnterTimestamp, and it
      says 'holding off' in the journal rather than waiting silently. Misses keep
      counting through the grace so an expiring grace acts at once. The per-poll
      is-active became one show returning both fields: 7.0ms -> 9.4ms, +0.12% of a
      core, measured not assumed.
      
      Also cures the watchdog of the same pgrep -x that cost protect a tenth of a
      core -- 52.9ms per poll while SC is up (3.0% of a core WHILE PLAYING) against
      20.8ms for one stateless /proc/*/comm pass, zero forks. Duplicated from
      parvagues-protect rather than shared: that script is copied to /usr/local/bin
      and runs as root, so it must not source anything from a user-writable repo.
      
      rig-doctor's check_protect now asks whether protection WORKS: the running
      daemon's live CapEff from /proc must include cap_dac_override, and the daemon's
      own --check must pass. It was green all through the day the guard could not
      write a single oom_score_adj, because it tested that two files existed.
      
      Verified end to end on the rig: protect caught pids created 9min after it
      started (oom:200->-1000 on sclang[257669]), and the watchdog held off at 6s and
      spent none of the rate limit. Suite 11/11 incl. a new slow-boot regression case;
      rig-doctor 46 checks, 0 fail.
      PLN (Algolia) authored
    • perf(protect): the guard was spending a tenth of a core to discover nothing had changed · a828d5f0
      219.1ms per 2s tick, 6 forks, with NOTHING running to protect. None of it was
      protection: three pgrep -x at 59ms each (pgrep reads cmdline for every process
      on the box, and it ran once per target), plus cat, two chrt -p reads and an
      unconditional prlimit per pid, each in its own command substitution.
      
      Now zero forks in the steady state: one bash pass over /proc/*/comm, sched from
      /proc/<pid>/stat fields 40/41, prlimit only when /proc/<pid>/limits says so.
      28.5ms/tick, 1.42% of a core at the unchanged 2s interval.
      
      The interval is deliberately untouched -- the 7.7x came from forks alone, so the
      responsiveness that re-protects scsynth before its first sound was not traded
      for battery.
      
      Verified differentially against the old pgrep path with a decoy fleet (ardour9,
      ArdourGUI, ardour-8.6 must match; ardour-decoy, sclang-notreally, scsynthx and a
      bash whose path contains 'ardour' must not) and against chrt -p / cat on live
      scsynth+sclang. Needs sudo tools/install-protect.sh to take effect.
      
      Also logs two findings from the same measurement pass: sc-watchdog restarts
      SuperDirt on every clean start (6s patience vs 8s boot) and two starts in five
      minutes hit StartLimitBurst, leaving the rig unstartable; and the other two
      reconcile loops cost 5.3% between them with real event sources available.
      PLN (Algolia) authored