livepilot 1.23.5 → 1.23.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,29 @@
1
1
  # Changelog
2
2
 
3
+ ## v1.23.6 — 2026-04-30
4
+
5
+ ### Fixed
6
+
7
+ - **`ensure_analyzer_on_master` cold-start disambiguation.** On a fresh Live boot the User Library browser cache is uncached, and `find_and_load_device("LivePilot_Analyzer")` can exceed the 20s recv timeout while BFS-ing the user library tree. The previous catch-block surfaced `status="install_required"` even though the .amxd is sitting at the canonical install path — sending the agent into a reinstall loop. New `_analyzer_amxd_installed_at_user_library()` filesystem check disambiguates: when the .amxd is present at `~/Music/Ableton/User Library/Presets/Audio Effects/Max Audio Effect/`, the tool now returns `status="cache_cold"` with a retry hint instead. Genuine missing-.amxd path still returns `install_required` correctly. (`mcp_server/tools/analyzer.py`)
8
+ - **`set_compressor_sidechain` diagnostic depth.** When `_find_sidechain_surface` returns None on Compressor2 (Live 12.3.6+), the raised error now includes `class=...` + `canonical_parent.class=...` + a widened child-attribute walk (`input_routings`, `routing_inputs` added to the probe). Lets the next live failure leave a tighter trail so the actual Compressor2 LOM shape can be confirmed in one round trip rather than a separate probe session. No behavior change for legacy Compressor (I) or the two known Compressor2 hypotheses. (`remote_script/LivePilot/mixing.py`)
9
+ - **`atlas_demo_story` production_decision class-name leak.** `harmonic-foundation` and `rhythmic-driver` role branches fell back to `primary_cls` when `user_name` was empty — producing useless prose like "InstrumentGroupDevice chosen as harmonic spine." Both branches now use `primary_uname or t_name or primary_cls`, matching the texture-role fallback that was already correct. (`mcp_server/atlas/demo_story.py`)
10
+ - **`atlas_extract_chain` Macro N labeling on M4L devices.** The rack-device path already resolved producer-named macros via `resolve_preset_for_device`, but the M4L (.amxd) device path emitted raw `Macro N`. Now both paths cross-reference the preset sidecar — PitchLoop89's "Spectral Stretch" gets the proper name instead of "Macro 2" so `set_device_parameter` resolves at execution time. (`mcp_server/atlas/extract_chain.py`)
11
+ - **Bundled atlas stats refresh.** `stats.enriched_devices` was stale at `87` (from a v1.21.x scan); the actual `enriched=True` flag count had grown to `135`. Recounted in place. No tool consumes the field with semantically-different behavior, but the stale stat had drifted past the soft-warn threshold's intent. (`mcp_server/atlas/device_atlas.json`)
12
+
13
+ ### Docs / drift
14
+
15
+ - **`docs/M4L_BRIDGE.md`**: amxd ping reference and bridge-cmd count corrected — was `version: "1.23.1"` and "30 commands" from prior releases; now `1.23.6` and `32`.
16
+ - **`livepilot/skills/livepilot-core/references/genre-vocabularies.md`**: added Synthwave / Retrowave / Outrun entry to match the 15-genre claim in CLAUDE.md/AGENTS.md (file had 14; the YAML packet at `concepts/genres/synthwave.yaml` had been the source-of-truth for v1.18+).
17
+
18
+ ### Tests
19
+
20
+ - `tests/test_ensure_analyzer_on_master.py` — 2 new tests under `TestColdBrowserCacheDisambiguation` covering the cache-cold branch (`.amxd` present → cache_cold) and the install-required branch (`.amxd` absent → install_required). Existing `test_returns_install_required_when_device_not_in_browser` updated to monkeypatch the new path-check helper so it exercises the genuinely-not-installed path regardless of dev-machine state.
21
+ - Total: **3409 passing**, 1 skipped, 0 failed (up from 3407 in v1.23.5).
22
+
23
+ ### Audit notes
24
+
25
+ A full read-only audit pass against `BUGS.md` + `BUGS_TESTING_2026-04-30.md` confirmed that 14 of the bugs documented in those files were already fixed in v1.23.4's `bugfixes-2026-04-26` commit batch but never marked closed in the testing doc. No-ops here, but worth noting that the 38-bug list in the testing file is now ~5 genuinely-open items.
26
+
3
27
  ## v1.23.5 — 2026-04-30
4
28
 
5
29
  ### Fixed (Remote Script reliability — credit: PR #35 reporter)
Binary file
@@ -34,7 +34,7 @@ outlets = 2; // 0: to udpsend (responses), 1: to buffer~/status
34
34
  // Single source of truth for the bridge version — bumped alongside the
35
35
  // rest of the release manifest. Surfaced in the UI via messnamed("livepilot_version", ...)
36
36
  // so the frozen .amxd visibly reports which build it was last exported from.
37
- var VERSION = "1.23.5";
37
+ var VERSION = "1.23.6";
38
38
 
39
39
  // ── State ──────────────────────────────────────────────────────────────────
40
40
 
@@ -1,2 +1,2 @@
1
1
  """LivePilot MCP Server — bridges MCP protocol to Ableton Live."""
2
- __version__ = "1.23.5"
2
+ __version__ = "1.23.6"
@@ -608,19 +608,25 @@ def demo_story(
608
608
  primary_uname = primary_device.get("user_name") or ""
609
609
  nonzero = _count_nonzero_macros(devices)
610
610
 
611
+ # BUG-E5: when user_name is empty AND primary device is a Group/Rack
612
+ # device, primary_cls reads as "InstrumentGroupDevice" /
613
+ # "AudioEffectGroupDevice" — useless prose like "InstrumentGroupDevice
614
+ # chosen as harmonic spine". Fall back to track name first, then class.
615
+ primary_label = primary_uname or t_name or primary_cls or "<unknown>"
616
+
611
617
  if t_type == "ReturnTrack":
612
618
  prod_decision = (
613
- f"{primary_cls} shared return — applies uniformly across all sends."
619
+ f"{primary_label} shared return — applies uniformly across all sends."
614
620
  )
615
621
  elif role == "harmonic-foundation":
616
622
  prod_decision = (
617
- f"{primary_uname or primary_cls} chosen as harmonic spine; "
623
+ f"{primary_label} chosen as harmonic spine; "
618
624
  f"{nonzero} macro(s) committed to specific values, "
619
625
  "suggesting deliberate timbral targeting."
620
626
  )
621
627
  elif role == "rhythmic-driver":
622
628
  prod_decision = (
623
- f"Drum rack '{primary_uname or primary_cls}' provides rhythmic drive. "
629
+ f"Drum rack '{primary_label}' provides rhythmic drive. "
624
630
  f"{nonzero} non-default macro values indicate custom tuning."
625
631
  )
626
632
  elif role == "texture":
@@ -15,7 +15,7 @@
15
15
  "max_for_live": 523,
16
16
  "clips": 1000,
17
17
  "current_project": 60,
18
- "enriched_devices": 87
18
+ "enriched_devices": 135
19
19
  },
20
20
  "devices": [
21
21
  {
@@ -492,18 +492,33 @@ def _emit_execution_steps(
492
492
  "device_index": device_index,
493
493
  })
494
494
  if fidelity != "structure-only" and macros:
495
+ # BUG-E2#1 (M4L path): producer-named macros on devices like
496
+ # PitchLoop89 ("Spectral Stretch") get raw "Macro N" labels here
497
+ # unless we cross-reference the preset sidecar — same as the rack
498
+ # path at line ~395. Without name resolution, set_device_parameter
499
+ # by name silently NOT-FOUNDs at execution time.
500
+ m4l_macro_names: dict[int, str] = {}
501
+ if uname and pack_name:
502
+ m4l_match = resolve_preset_for_device(pack_name, cls, uname)
503
+ m4l_macro_names = m4l_match.get("macro_names") or {}
495
504
  macro_subset = (
496
505
  _get_nonzero_macros(macros)
497
506
  if fidelity == "exact"
498
507
  else _top_k_macros_by_deviation(macros, k=5)
499
508
  )
500
509
  for m in macro_subset:
510
+ resolved_name = m4l_macro_names.get(m["index"]) or f"Macro {m['index'] + 1}"
511
+ source_tag = (
512
+ "als-parse+adg-parse"
513
+ if resolved_name != f"Macro {m['index'] + 1}"
514
+ else "als-parse"
515
+ )
501
516
  steps.append({
502
517
  "action": "set_device_parameter",
503
518
  "device_index": device_index,
504
- "parameter_name": f"Macro {m['index'] + 1}",
519
+ "parameter_name": resolved_name,
505
520
  "value": round(_safe_float(m.get("value", "0")), 2),
506
- "comment": f"M4L macro {m['index'] + 1} [SOURCE: als-parse]",
521
+ "comment": f"M4L macro {m['index'] + 1} [SOURCE: {source_tag}]",
507
522
  })
508
523
  return steps, warnings
509
524
 
@@ -2003,6 +2003,31 @@ def _load_analyzer_impl(ctx, track_index: int, device_name: str,
2003
2003
  )
2004
2004
 
2005
2005
 
2006
+ def _analyzer_amxd_installed_at_user_library() -> bool:
2007
+ """BUG-T#1: distinguish cold-browser-cache from genuinely-not-installed.
2008
+
2009
+ On a fresh Live boot the User Library browser tree is uncached, and
2010
+ find_and_load_device can exceed the recv_timeout while doing the deep
2011
+ BFS for ``LivePilot_Analyzer.amxd``. The catch-block then surfaces
2012
+ ``install_required`` even though the .amxd is sitting at the canonical
2013
+ install path. This helper does a cheap filesystem check so the caller
2014
+ can return ``cache_cold`` (retry hint) instead of misleading the agent
2015
+ into a reinstall loop.
2016
+
2017
+ Returns True iff a file named ``LivePilot_Analyzer.amxd`` exists under
2018
+ the User Library Max Audio Effect directory.
2019
+ """
2020
+ from pathlib import Path
2021
+ try:
2022
+ path = (Path.home()
2023
+ / "Music" / "Ableton" / "User Library"
2024
+ / "Presets" / "Audio Effects" / "Max Audio Effect"
2025
+ / f"{_ANALYZER_DEVICE_NAME}.amxd")
2026
+ return path.is_file()
2027
+ except Exception:
2028
+ return False
2029
+
2030
+
2006
2031
  @mcp.tool()
2007
2032
  def ensure_analyzer_on_master(ctx: Context) -> dict:
2008
2033
  """Idempotent pre-flight: load LivePilot_Analyzer on master if missing.
@@ -2077,8 +2102,28 @@ def ensure_analyzer_on_master(ctx: Context) -> dict:
2077
2102
  allow_duplicate=False,
2078
2103
  )
2079
2104
  except Exception as exc:
2080
- # Typical path: device not in browser (user hasn't installed via
2081
- # install_m4l_device yet).
2105
+ # BUG-T#1: distinguish two failure modes that look identical here:
2106
+ # (a) genuinely not installed — install_m4l_device path is right
2107
+ # (b) installed but Ableton's browser cache is cold (first call
2108
+ # after Live boot can exceed recv_timeout while BFS-ing the
2109
+ # User Library tree). In (b), pointing the agent at
2110
+ # install_m4l_device is wrong — the file is already there.
2111
+ if _analyzer_amxd_installed_at_user_library():
2112
+ return {
2113
+ "status": "cache_cold",
2114
+ "error": str(exc),
2115
+ "hint": (
2116
+ "LivePilot_Analyzer.amxd is installed at "
2117
+ "~/Music/Ableton/User Library/Presets/Audio Effects/"
2118
+ "Max Audio Effect/, but the browser search timed out — "
2119
+ "Ableton's User Library cache is typically cold on the "
2120
+ "first call after a fresh Live boot. Retry "
2121
+ "ensure_analyzer_on_master once; the second call usually "
2122
+ "completes in <1s. If it keeps timing out, click any "
2123
+ "User Library entry in Ableton's browser to warm the "
2124
+ "cache, then retry."
2125
+ ),
2126
+ }
2082
2127
  return {
2083
2128
  "status": "install_required",
2084
2129
  "error": str(exc),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "livepilot",
3
- "version": "1.23.5",
3
+ "version": "1.23.6",
4
4
  "mcpName": "io.github.dreamrec/livepilot",
5
5
  "description": "Agentic production system for Ableton Live 12 \u2014 453 tools, 54 domains, 44 semantic moves. Device atlas (5264 devices, 120 enriched, 7 indexes), Splice intelligence (gRPC + GraphQL describe-a-sound + preview + collections + presets), 9-band spectral perception auto-loaded via ensure_analyzer_on_master, Creative Director skill, technique memory, 12 creative intelligence engines",
6
6
  "author": "Pilot Studio",
@@ -5,7 +5,7 @@ Entry point for the ControlSurface. Ableton calls create_instance(c_instance)
5
5
  when this script is selected in Preferences > Link, Tempo & MIDI.
6
6
  """
7
7
 
8
- __version__ = "1.23.5"
8
+ __version__ = "1.23.6"
9
9
 
10
10
  from _Framework.ControlSurface import ControlSurface
11
11
  from . import router
@@ -371,11 +371,17 @@ def _find_sidechain_surface(device):
371
371
 
372
372
 
373
373
  def _collect_routing_diagnostic(device):
374
- """Return a CSV of routing/sidechain attrs on device + likely children.
374
+ """Return a structured trail of routing/sidechain attrs on device + likely children.
375
375
 
376
376
  Used as a breadcrumb in the error message when _find_sidechain_surface
377
377
  returns None, so the first failing call tells us what the current Live
378
378
  build actually exposes without a separate probe session.
379
+
380
+ BUG-A3 v2 (2026-04-30): the diagnostic now includes class_name and the
381
+ canonical_parent class so the next live-probe operator can immediately
382
+ tell whether they're looking at a Compressor / Compressor2 / something
383
+ else. Also widens the child-name search to include Live 12.x patterns
384
+ (input_routings, routing_inputs).
379
385
  """
380
386
  def _attrs(obj, prefix):
381
387
  try:
@@ -388,8 +394,23 @@ def _collect_routing_diagnostic(device):
388
394
  return []
389
395
  return [prefix + n for n in names]
390
396
 
397
+ parts = []
398
+ try:
399
+ parts.append("class=%s" % device.class_name)
400
+ except Exception:
401
+ pass
402
+ try:
403
+ cp = getattr(device, "canonical_parent", None)
404
+ if cp is not None:
405
+ parts.append("canonical_parent.class=%s" % type(cp).__name__)
406
+ except Exception:
407
+ pass
408
+
391
409
  found = list(_attrs(device, "device."))
392
- for child_name in ("sidechain_input", "input", "sidechain", "routing"):
410
+ # Probe widened in v2 to include Live 12.x patterns. Order is
411
+ # least-to-most exotic so legacy shapes take precedence.
412
+ for child_name in ("sidechain_input", "input", "sidechain", "routing",
413
+ "input_routings", "routing_inputs"):
393
414
  try:
394
415
  child = getattr(device, child_name, None)
395
416
  except Exception:
@@ -397,7 +418,12 @@ def _collect_routing_diagnostic(device):
397
418
  if child is None:
398
419
  continue
399
420
  found.extend(_attrs(child, "device.%s." % child_name))
400
- return ", ".join(found) if found else "<none>"
421
+
422
+ if found:
423
+ parts.append("attrs=[%s]" % ", ".join(found))
424
+ else:
425
+ parts.append("attrs=<none>")
426
+ return " | ".join(parts) if parts else "<none>"
401
427
 
402
428
 
403
429
  @register("set_compressor_sidechain")
package/server.json CHANGED
@@ -6,7 +6,7 @@
6
6
  "url": "https://github.com/dreamrec/LivePilot",
7
7
  "source": "github"
8
8
  },
9
- "version": "1.23.5",
9
+ "version": "1.23.6",
10
10
  "packages": [
11
11
  {
12
12
  "registryType": "npm",