livepilot 1.14.1 → 1.15.0-beta

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,47 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.15.0-beta — Live 12.4 replace_sample native (April 21 2026)
4
+
5
+ First Live 12.4 beta support release. Adds a native fast path for
6
+ SimplerDevice.replace_sample(path) while preserving 100% backward
7
+ compatibility for 12.0-12.3.x users.
8
+
9
+ **Tool count**: unchanged at 403.
10
+ **Domain count**: unchanged at 52.
11
+ **Tests**: 2503 passed, 1 skipped, 0 regressions (+13 new from this release).
12
+
13
+ ### Added
14
+ - **Live 12.4 support (beta):** `SimplerDevice.replace_sample(path)` native
15
+ LOM path is now used automatically on Live 12.4+. Handles empty Simplers —
16
+ fixes the long-standing workaround documented in
17
+ `feedback_load_browser_item_is_source_of_truth.md`.
18
+ - New capability tier `"collaborative"` (Live 12.4+) exposed via
19
+ `LiveVersionCapabilities.capability_tier` and `.has_replace_sample_native`.
20
+ - Remote Script: new `replace_sample_native` handler.
21
+ - MCP server: new `_live_caps(ctx)` helper with lazy version-capability
22
+ caching on the lifespan context.
23
+ - Registered `replace_sample_native` in `mcp_server/runtime/remote_commands.py`
24
+ REMOTE_COMMANDS (required by the boundary audit contract).
25
+
26
+ ### Changed
27
+ - `replace_simpler_sample` and `load_sample_to_simpler` now route to the
28
+ native path when available and fall back to the M4L-bridge path
29
+ otherwise. Tool signatures, argument names, and return shapes unchanged.
30
+
31
+ ### Backward Compatibility
32
+ - Live 12.0–12.3.x: zero behavior change. All routing still goes through
33
+ the M4L bridge.
34
+ - Live 12.4+: native path preferred; bridge used only on fallback.
35
+
36
+ ### Verification status
37
+ - Full test suite: 2503 passed, 0 failures.
38
+ - Backward compat on Live 12.4: verified in-session — `replace_simpler_sample`
39
+ and `load_sample_to_simpler` both work via the bridge path on 12.4 (legacy
40
+ flow intact).
41
+ - Native E2E on Live 12.4 empty-Simpler case: deferred until the plugin
42
+ swap activates the worktree's MCP code. Unit tests prove the routing
43
+ logic and the native handler wiring.
44
+
3
45
  ## 1.14.1 — reload_handlers workflow + device/mixing fixes (April 21 2026)
4
46
 
5
47
  Patch release that lands the post-1.14.0 audit work: one new diagnostics
@@ -13,7 +55,7 @@ tool, three bug fixes, and a new plugin-sync verification script.
13
55
 
14
56
  - Replaces the manual "toggle Control Surface in Live → Preferences →
15
57
  Link/MIDI" step that every Remote Script edit required. New workflow:
16
- after `node installer/install.js`, call `reload_handlers` via the MCP
58
+ after `npx livepilot --install`, call `reload_handlers` via the MCP
17
59
  tool. The Remote Script side uses `pkgutil` + `importlib.reload()` to
18
60
  re-fire all `@register` decorators in place in <1s, without dropping
19
61
  the MCP TCP connection on port 9878.
Binary file
@@ -1,2 +1,2 @@
1
1
  """LivePilot MCP Server — bridges MCP protocol to Ableton Live."""
2
- __version__ = "1.14.1"
2
+ __version__ = "1.15.0-beta"
@@ -79,8 +79,10 @@ class LiveVersionCapabilities:
79
79
 
80
80
  @property
81
81
  def capability_tier(self) -> str:
82
- """Human-readable tier: core | enhanced_arrangement | full_intelligence."""
83
- if self._version_tuple >= (12, 3, 0):
82
+ """Human-readable tier: core | enhanced_arrangement | full_intelligence | collaborative."""
83
+ if self._version_tuple >= (12, 4, 0):
84
+ return "collaborative"
85
+ elif self._version_tuple >= (12, 3, 0):
84
86
  return "full_intelligence"
85
87
  elif self._version_tuple >= (12, 1, 10):
86
88
  return "enhanced_arrangement"
@@ -117,6 +117,8 @@ REMOTE_COMMANDS: frozenset[str] = frozenset({
117
117
  "get_appointed_device",
118
118
  # ping (built-in)
119
119
  "ping",
120
+ # Live 12.4+ native Simpler sample replacement (Collaborative tier)
121
+ "replace_sample_native",
120
122
  })
121
123
 
122
124
  # M4L bridge commands — routed through TCP but handled by livepilot_bridge.js
@@ -64,6 +64,71 @@ def _enrich_slice_response(response: Optional[dict]) -> Optional[dict]:
64
64
  return enriched
65
65
 
66
66
 
67
+ def _live_caps(ctx):
68
+ """Read (or lazily compute + cache) LiveVersionCapabilities on the context.
69
+
70
+ On first call, queries Ableton via get_session_info and caches the
71
+ result in ``ctx.lifespan_context["_live_caps"]``. Subsequent calls
72
+ short-circuit to the cache. If Ableton is unreachable, falls back to
73
+ 12.0.0 (conservative — all new-version gates return False).
74
+
75
+ This mirrors the on-demand pattern in
76
+ mcp_server/runtime/capability_probe.py and avoids adding a startup
77
+ round-trip to the lifespan.
78
+ """
79
+ from mcp_server.runtime.live_version import LiveVersionCapabilities
80
+
81
+ lsc = ctx.lifespan_context
82
+ cached = lsc.get("_live_caps")
83
+ if cached is not None:
84
+ return cached
85
+
86
+ version_str = "12.0.0"
87
+ ableton = lsc.get("ableton")
88
+ if ableton is not None:
89
+ try:
90
+ info = ableton.send_command("get_session_info") or {}
91
+ version_str = info.get("live_version", "12.0.0")
92
+ except Exception:
93
+ pass
94
+
95
+ caps = LiveVersionCapabilities.from_version_string(version_str)
96
+ lsc["_live_caps"] = caps
97
+ return caps
98
+
99
+
100
+ async def _try_native_replace_sample(ctx, track_index: int, device_index: int,
101
+ file_path: str):
102
+ """Attempt the Live 12.4+ native SimplerDevice.replace_sample path.
103
+
104
+ Returns the remote-script response dict on success, or None if the
105
+ native path is unavailable (pre-12.4) or failed (caller should fall
106
+ back to the M4L-bridge path).
107
+
108
+ A native "failure" is any of: gate closed, dispatch exception, non-dict
109
+ response, error field present, or missing sample_loaded flag.
110
+ """
111
+ caps = _live_caps(ctx)
112
+ if not caps.has_replace_sample_native:
113
+ return None
114
+ ableton = ctx.lifespan_context["ableton"]
115
+ try:
116
+ resp = ableton.send_command("replace_sample_native", {
117
+ "track_index": track_index,
118
+ "device_index": device_index,
119
+ "file_path": file_path,
120
+ })
121
+ except Exception:
122
+ return None
123
+ if not isinstance(resp, dict):
124
+ return None
125
+ if "error" in resp:
126
+ return None
127
+ if not resp.get("sample_loaded"):
128
+ return None
129
+ return resp
130
+
131
+
67
132
  @mcp.tool()
68
133
  async def reconnect_bridge(ctx: Context) -> dict:
69
134
  """Attempt to reconnect the M4L UDP bridge (port 9880).
@@ -350,12 +415,27 @@ async def replace_simpler_sample(
350
415
  _require_analyzer(cache)
351
416
  bridge = _get_m4l(ctx)
352
417
  ableton = ctx.lifespan_context["ableton"]
418
+
419
+ # Live 12.4+: prefer the native SimplerDevice.replace_sample path.
420
+ native = await _try_native_replace_sample(
421
+ ctx, track_index, device_index, file_path
422
+ )
423
+ if native is not None:
424
+ hygiene = await _simpler_post_load_hygiene(
425
+ bridge, ableton, track_index, device_index, file_path
426
+ )
427
+ if not hygiene.get("verified"):
428
+ return hygiene
429
+ result = dict(native)
430
+ result.update(hygiene)
431
+ result["method"] = "native_12_4" # preserved in case hygiene ever adds its own key
432
+ return result
433
+
434
+ # Pre-12.4 fallback: M4L bridge path (unchanged behavior).
353
435
  result = await bridge.send_command(
354
436
  "replace_simpler_sample", track_index, device_index, file_path
355
437
  )
356
438
 
357
- # Validate the response — the bridge may report success even when the
358
- # sample silently failed to load (e.g., empty Simpler, bad path)
359
439
  if "error" in result:
360
440
  return result
361
441
  if not result.get("sample_loaded"):
@@ -364,8 +444,6 @@ async def replace_simpler_sample(
364
444
  "has a sample loaded — replace_sample silently fails on empty Simplers."
365
445
  }
366
446
 
367
- # Verify by reading back the device name — guards against the silent
368
- # failure mode where the bridge reports success but keeps the placeholder.
369
447
  hygiene = await _simpler_post_load_hygiene(
370
448
  bridge, ableton, track_index, device_index, file_path
371
449
  )
@@ -405,9 +483,39 @@ async def load_sample_to_simpler(
405
483
  cache = _get_spectral(ctx)
406
484
  _require_analyzer(cache)
407
485
  bridge = _get_m4l(ctx)
486
+ ableton = ctx.lifespan_context["ableton"]
487
+
488
+ # Live 12.4+: create an empty Simpler via insert_device, then use the
489
+ # native replace_sample path. Skips the dummy-sample bootstrap entirely.
490
+ caps = _live_caps(ctx)
491
+ if caps.has_replace_sample_native:
492
+ try:
493
+ ins = ableton.send_command("insert_device", {
494
+ "track_index": track_index,
495
+ "device_name": "Simpler",
496
+ })
497
+ except Exception:
498
+ ins = None
499
+ if isinstance(ins, dict) and "error" not in ins:
500
+ actual_device_index = ins.get("device_index", device_index)
501
+ native = await _try_native_replace_sample(
502
+ ctx, track_index, actual_device_index, file_path
503
+ )
504
+ if native is not None:
505
+ hygiene = await _simpler_post_load_hygiene(
506
+ bridge, ableton, track_index, actual_device_index, file_path
507
+ )
508
+ if not hygiene.get("verified"):
509
+ return hygiene
510
+ result = dict(native)
511
+ result.update(hygiene)
512
+ result["method"] = "native_12_4"
513
+ result["device_index"] = actual_device_index
514
+ result["track_index"] = track_index
515
+ return result
516
+ # Fall through to the legacy bootstrap path below on any failure.
408
517
 
409
518
  # Step 1: Load a sample from the browser to create Simpler with content
410
- ableton = ctx.lifespan_context["ableton"]
411
519
  try:
412
520
  search = ableton.send_command("search_browser", {
413
521
  "path": "samples",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "livepilot",
3
- "version": "1.14.1",
3
+ "version": "1.15.0-beta",
4
4
  "mcpName": "io.github.dreamrec/livepilot",
5
5
  "description": "Agentic production system for Ableton Live 12 — 403 tools, 52 domains. Device atlas (1305 devices), sample engine (Splice + browser + filesystem), auto-composition, spectral perception, technique memory, creative intelligence (12 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.14.1"
8
+ __version__ = "1.15.0-beta"
9
9
 
10
10
  from _Framework.ControlSurface import ControlSurface
11
11
  from . import router
@@ -26,6 +26,7 @@ from . import follow_actions # noqa: F401 — registers follow action handle
26
26
  from . import grooves # noqa: F401 — registers groove pool handlers (11+)
27
27
  from . import take_lanes # noqa: F401 — registers take lane handlers (12.0+ read, 12.2+ write)
28
28
  from . import clip_automation # noqa: F401 — registers clip automation handlers
29
+ from . import simpler_sample # noqa: F401 — registers replace_sample_native (12.4+)
29
30
  from . import version_detect # noqa: F401 — version detection
30
31
 
31
32
 
@@ -0,0 +1,98 @@
1
+ # remote_script/LivePilot/simpler_sample.py
2
+ """
3
+ LivePilot — Simpler sample replacement via the native Live 12.4 LOM API.
4
+
5
+ Exposes a ``replace_sample_native`` command that calls
6
+ ``SimplerDevice.replace_sample(absolute_path)`` directly on the main thread.
7
+ Unlike the M4L-bridge path, this handler works on empty Simplers (the whole
8
+ reason 12.4 added the native API) and does not require a Max for Live
9
+ device in the Set.
10
+
11
+ Version-gated on ``replace_sample_native`` (12.4.0+). On earlier versions
12
+ the handler returns a STATE_ERROR; callers (MCP tools) are expected to
13
+ fall back to the bridge path.
14
+ """
15
+
16
+ from .router import register
17
+ from .version_detect import has_feature, version_string
18
+
19
+
20
+ @register("replace_sample_native")
21
+ def replace_sample_native(song, params):
22
+ """Replace the sample in a Simpler device using the Live 12.4+ native API.
23
+
24
+ params dict keys:
25
+ track_index (int): 0-based index into song.tracks.
26
+ device_index (int): 0-based index into the track's devices.
27
+ file_path (str): absolute path to the audio file to load.
28
+
29
+ Returns on success:
30
+ sample_loaded (bool): True.
31
+ track_index (int): echoed from input.
32
+ device_index (int): echoed from input.
33
+ method (str): "native_12_4".
34
+ live_version (str): detected Live version at call time.
35
+
36
+ Returns on error:
37
+ error (str): human-readable message.
38
+ code (str): STATE_ERROR | INDEX_ERROR | INVALID_PARAM | INTERNAL.
39
+ """
40
+ if not has_feature("replace_sample_native"):
41
+ return {
42
+ "error": (
43
+ "replace_sample_native requires Live 12.4+. "
44
+ "Detected: " + version_string() + ". "
45
+ "Use the M4L-bridge replace_simpler_sample path instead."
46
+ ),
47
+ "code": "STATE_ERROR",
48
+ }
49
+
50
+ try:
51
+ track_index = int(params["track_index"])
52
+ device_index = int(params["device_index"])
53
+ file_path = str(params["file_path"])
54
+ except (KeyError, TypeError, ValueError) as exc:
55
+ return {
56
+ "error": "Invalid params: " + str(exc),
57
+ "code": "INVALID_PARAM",
58
+ }
59
+
60
+ tracks = list(song.tracks)
61
+ if track_index < 0 or track_index >= len(tracks):
62
+ return {
63
+ "error": "track_index " + str(track_index) + " out of range",
64
+ "code": "INDEX_ERROR",
65
+ }
66
+
67
+ track = tracks[track_index]
68
+ devices = list(track.devices)
69
+ if device_index < 0 or device_index >= len(devices):
70
+ return {
71
+ "error": "device_index " + str(device_index) + " out of range",
72
+ "code": "INDEX_ERROR",
73
+ }
74
+
75
+ device = devices[device_index]
76
+ class_name = getattr(device, "class_name", "")
77
+ if class_name != "SimplerDevice":
78
+ return {
79
+ "error": "Device at [" + str(track_index) + "][" + str(device_index) + "] is "
80
+ + class_name + ", not Simpler",
81
+ "code": "INVALID_PARAM",
82
+ }
83
+
84
+ try:
85
+ device.replace_sample(file_path)
86
+ except Exception as exc:
87
+ return {
88
+ "error": "SimplerDevice.replace_sample failed: " + str(exc),
89
+ "code": "INTERNAL",
90
+ }
91
+
92
+ return {
93
+ "sample_loaded": True,
94
+ "track_index": track_index,
95
+ "device_index": device_index,
96
+ "method": "native_12_4",
97
+ "live_version": version_string(),
98
+ }
package/server.json CHANGED
@@ -6,12 +6,12 @@
6
6
  "url": "https://github.com/dreamrec/LivePilot",
7
7
  "source": "github"
8
8
  },
9
- "version": "1.14.1",
9
+ "version": "1.15.0-beta",
10
10
  "packages": [
11
11
  {
12
12
  "registryType": "npm",
13
13
  "identifier": "livepilot",
14
- "version": "1.14.1",
14
+ "version": "1.15.0-beta",
15
15
  "transport": {
16
16
  "type": "stdio"
17
17
  }