livepilot 1.23.2 → 1.23.3

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,36 @@
1
1
  # Changelog
2
2
 
3
+ ## v1.23.3 — 2026-04-25
4
+
5
+ ### Fixed
6
+ - **`classify_simpler_slices` now classifies slices when `file_path` is omitted (the v1.12 follow-up, finally closed).** The tool's docstring promotes "Always run this before programming drum patterns on a sliced break", but the file_path lookup had been a no-op since v1.11 — wrapped in `try/except` with the comment *"Bridge command may not exist yet"* and a fallback error message *"v1.12 follow-up"*. End-to-end verification against Ableton 12.4 + a Splice sliced break ("ff_mch_122_drum_loop_first_perc.wav"): all 23 slices get correct `label` (KICK/SNARE/HAT/ghost) + spectral breakdown, no manual `file_path` argument needed.
7
+ - **Pivot from M4L bridge to Remote Script TCP** for the file-path lookup. The bridge round-trip surfaced a chunked-response correlation issue under live testing (the second successive `bridge.send_command` returned the *previous* command's payload). The Remote Script handler reads `device.sample.file_path` directly via Live's Python LOM — no UDP packets, no chunk reassembly, no ambiguity. Implementation:
8
+ - `remote_script/LivePilot/simpler_sample.py` — adds `@register("get_simpler_file_path")` handler. Resolves the SimplerDevice (with Drum Rack chain support via optional `chain_index` / `nested_device_index`), reads `device.sample.file_path`, returns `{file_path, track_index, device_index, name, ...}`.
9
+ - `mcp_server/runtime/remote_commands.py` — registers `get_simpler_file_path` in `REMOTE_COMMANDS` (TCP path, primary). The pre-existing entry in `BRIDGE_COMMANDS` stays for forward-compat — `execution_router.classify_step` puts REMOTE_COMMANDS first, so plans route TCP, but the bridge case is still callable as a fallback.
10
+ - `mcp_server/tools/analyzer.py::classify_simpler_slices` — Remote Script TCP call is primary; M4L bridge `bridge.send_command("get_simpler_file_path", …)` is the fallback path. Surfaces the Remote Script's specific error verbatim (e.g. "Simpler.sample.file_path is empty — sample may be embedded") instead of the generic v1.12 message.
11
+ - `m4l_device/livepilot_bridge.js` — `case "get_simpler_file_path"` + `cmd_get_simpler_file_path()` remain in the .amxd freeze for the fallback path.
12
+ - **ff29381 (carried forward)**: `insert_rack_chain` returns `chain_index` so `add_drum_rack_pad` no longer clobbers chain 0 on the second pad. Live-verified against an empty Drum Rack on Ableton 12.4 — three sequential `insert_rack_chain` calls (`chain_index=0`, then `1`, then `0` with `chain_count=3` after position-0 insert).
13
+
14
+ ### Improved
15
+ - **CI freshness gate is now source-of-truth-aligned.** The `amxd-freeze-drift` job in `.github/workflows/ci.yml` previously grepped for `"version": "X.Y.Z"` (the runtime ping JSON form), which only landed in the .amxd via post-export binary patching. Since the v1.20-era refactor to `"version": VERSION` (variable reference), every release needed manual byte-patching to satisfy this gate. Now greps for `var VERSION = "X.Y.Z"` — the source-of-truth declaration in `bridge.js` that always survives a Max freeze. No more binary-patching hack required for plain version bumps.
16
+
17
+ ### Bridge surface
18
+ - 31 → 32 cases (`get_simpler_file_path` added — used as forward-compat fallback only; primary path is Remote Script TCP).
19
+ - ⚠️ Re-freeze required for the .amxd. New freeze at `m4l_device/LivePilot_Analyzer.amxd` (md5 changed) — distributed to `~/Music/Ableton/User Library/Presets/Audio Effects/Max Audio Effect/` and `~/Documents/Max 9/Library/`. Workflow memory `feedback_amxd_edit_via_maxpat.md` updated to clarify the project-folder JS (`~/Documents/Max 9/Max for Live Devices/<DeviceName> Project/code/livepilot_bridge.js`) is the authoritative path Max freezes from — NOT the user search path `~/Documents/Max 9/Code/`.
20
+
21
+ ### Remote Script
22
+ - 191 → 193 handlers. `+1` for `get_simpler_file_path` (this release). The other +1 came from a prior session's reload-handler registration count.
23
+
24
+ ### Tests
25
+ - `test_bugfixes_2026_04_25.py` (locks the `chain_index` contract from ff29381) — passes.
26
+ - `test_analyzer_tools.py::test_classify_simpler_slices_returns_error_when_no_file_path` — updated to reflect the v1.23.3 error contract: when both Remote Script and bridge fall back, the Remote Script's specific error (e.g. "Simpler.sample.file_path is empty") is surfaced verbatim.
27
+ - Full suite: 3180 passed, 1 skipped, 0 failed.
28
+
29
+ ### Live-test verification log (Ableton 12.4)
30
+ - `classify_simpler_slices(track=4, device=0)` on a 23-slice WAV drum break with no `file_path` argument → returns full classification (KICK/HAT/ghost labels + spectral % per slice). ✓
31
+ - `classify_simpler_slices` with explicit `file_path` argument → bypasses lookup, classifies normally. ✓
32
+ - `insert_rack_chain` end-to-end on empty Drum Rack → `chain_index` derivation correct in all 3 branches (append-on-empty, append-on-N, position-0-on-N). ✓
33
+
3
34
  ## v1.23.2 — 2026-04-25
4
35
 
5
36
  ### Fixed
package/README.md CHANGED
@@ -103,7 +103,7 @@ Most MCP servers are tool collections — they execute commands. LivePilot is an
103
103
 
104
104
  **MCP Server** (`mcp_server/`) — Python FastMCP server. Validates inputs, routes commands to the Remote Script over TCP, manages the M4L bridge, runs the atlas, sample engine, composer, and all intelligence engines. This is what your AI client connects to.
105
105
 
106
- **M4L Bridge** (`m4l_device/`) — Optional Max for Live Audio Effect on the master track. Provides deep LOM access through Max's LiveAPI that the ControlSurface API can't reach. UDP 9880 (M4L to server) carries spectral data and LiveAPI responses. OSC 9881 (server to M4L) sends commands. The 38 spectral/analyzer tools strictly require the bridge; device and sample tools that call the bridge also have graceful fallbacks, so core functionality works without it. Backed by 31 bridge commands for hidden parameters, Simpler internals, warp markers, display values, and Simpler warp / Compressor sidechain writes that live on child objects Python can't reach.
106
+ **M4L Bridge** (`m4l_device/`) — Optional Max for Live Audio Effect on the master track. Provides deep LOM access through Max's LiveAPI that the ControlSurface API can't reach. UDP 9880 (M4L to server) carries spectral data and LiveAPI responses. OSC 9881 (server to M4L) sends commands. The 38 spectral/analyzer tools strictly require the bridge; device and sample tools that call the bridge also have graceful fallbacks, so core functionality works without it. Backed by 32 bridge commands for hidden parameters, Simpler internals, warp markers, display values, and Simpler warp / Compressor sidechain writes that live on child objects Python can't reach.
107
107
 
108
108
  **Device Atlas** (`mcp_server/atlas/`) — In-memory indexed JSON database. 5264 devices with browser URIs (bundled baseline), 120 enriched with YAML sonic intelligence profiles (mood, genre, texture, recommended chains). 7 indexes: by_id, by_name, by_uri, by_category, by_tag, by_genre, by_pack. Reverse-index `device_techniques_index.json` powers `atlas_techniques_for_device` (146 cross-references across 58 devices). The AI never hallucinates a device name or preset — it always resolves against the atlas first. **v1.22.0+**: run `scan_full_library` after install to index YOUR packs + User Library + plugins into `~/.livepilot/atlas/device_atlas.json` — your personal atlas overrides the baseline and survives npm updates.
109
109
 
@@ -202,7 +202,7 @@ Every engine follows: **measure before → act → measure after → compare**.
202
202
 
203
203
  <br>
204
204
 
205
- ### M4L Bridge — 38 analyzer tools `[optional]`, 31 bridge commands
205
+ ### M4L Bridge — 38 analyzer tools `[optional]`, 32 bridge commands
206
206
 
207
207
  The M4L Analyzer sits on the master track. UDP 9880 carries spectral data to the server. OSC 9881 sends commands back. The `ensure_analyzer_on_master` pre-flight (v1.20.3) loads the analyzer idempotently on first use — call it once at session start and forget about it.
208
208
 
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.2";
37
+ var VERSION = "1.23.3";
38
38
 
39
39
  // ── State ──────────────────────────────────────────────────────────────────
40
40
 
@@ -142,6 +142,9 @@ function dispatch(cmd, args) {
142
142
  case "get_simpler_slices":
143
143
  cmd_get_simpler_slices(args);
144
144
  break;
145
+ case "get_simpler_file_path":
146
+ cmd_get_simpler_file_path(args);
147
+ break;
145
148
  case "crop_simpler":
146
149
  cmd_simpler_action(args, "crop");
147
150
  break;
@@ -975,6 +978,41 @@ function cmd_get_simpler_slices(args) {
975
978
  });
976
979
  }
977
980
 
981
+ function cmd_get_simpler_file_path(args) {
982
+ // Resolves the absolute file path of a Simpler's loaded sample.
983
+ // Mirrors cmd_get_clip_file_path's shape so analyzer.py can route both
984
+ // through the same response handler. Closes the v1.12 follow-up that
985
+ // left classify_simpler_slices unable to do its primary job
986
+ // (returning slice geometry without classification labels) when
987
+ // file_path wasn't passed explicitly.
988
+ var track_idx = parseInt(args[0]);
989
+ var device_idx = parseInt(args[1]);
990
+ var path = build_device_path(track_idx, device_idx);
991
+
992
+ cursor_a.goto(path);
993
+ if (cursor_a.get("class_name").toString() !== "OriginalSimpler") {
994
+ send_response({"error": "Not a Simpler device"});
995
+ return;
996
+ }
997
+
998
+ try {
999
+ cursor_b.goto(path + " sample");
1000
+ var sample_path = cursor_b.get("file_path").toString();
1001
+ if (!sample_path || sample_path === "0") {
1002
+ send_response({"error": "Simpler has no sample loaded"});
1003
+ return;
1004
+ }
1005
+ send_response({
1006
+ "track": track_idx,
1007
+ "device": device_idx,
1008
+ "file_path": _to_posix_path(sample_path),
1009
+ "name": cursor_a.get("name").toString()
1010
+ });
1011
+ } catch(e) {
1012
+ send_response({"error": "Failed to read Simpler sample path: " + e.message});
1013
+ }
1014
+ }
1015
+
978
1016
  function cmd_simpler_action(args, action) {
979
1017
  var track_idx = parseInt(args[0]);
980
1018
  var device_idx = parseInt(args[1]);
@@ -1,2 +1,2 @@
1
1
  """LivePilot MCP Server — bridges MCP protocol to Ableton Live."""
2
- __version__ = "1.23.2"
2
+ __version__ = "1.23.3"
@@ -122,6 +122,13 @@ REMOTE_COMMANDS: frozenset[str] = frozenset({
122
122
  "ping",
123
123
  # Live 12.4+ native Simpler sample replacement (Collaborative tier)
124
124
  "replace_sample_native",
125
+ # v1.23.3: read Simpler.sample.file_path directly via Python LOM —
126
+ # primary path for classify_simpler_slices' auto-resolution. Beats
127
+ # the M4L bridge round-trip (which had a chunked-response correlation
128
+ # issue in live testing). The bridge case `get_simpler_file_path`
129
+ # remains as a forward-compat fallback in case Remote Script is
130
+ # somehow stale.
131
+ "get_simpler_file_path",
125
132
  })
126
133
 
127
134
  # M4L bridge commands — routed through TCP but handled by livepilot_bridge.js
@@ -130,6 +137,9 @@ BRIDGE_COMMANDS: frozenset[str] = frozenset({
130
137
  "get_params", "get_hidden_params", "get_auto_state", "walk_rack",
131
138
  "get_chains_deep", "get_track_cpu", "get_selected", "get_key",
132
139
  "get_clip_file_path", "replace_simpler_sample", "get_simpler_slices",
140
+ "get_simpler_file_path", # v1.23.3 — closes the v1.12 follow-up that
141
+ # left classify_simpler_slices unable to
142
+ # auto-resolve the Simpler's sample path
133
143
  "crop_simpler", "reverse_simpler", "warp_simpler",
134
144
  "get_warp_markers", "add_warp_marker", "move_warp_marker",
135
145
  "remove_warp_marker", "capture_audio", "capture_stop",
@@ -1018,10 +1018,11 @@ async def classify_simpler_slices(
1018
1018
 
1019
1019
  Parameters:
1020
1020
  track_index, device_index: the Simpler to analyze
1021
- file_path: (optional) explicit WAV path. If omitted, attempts
1022
- lookup via the bridge. Bridge-native resolution is limited in
1023
- v1.11 when the sample lives in the Core Library, pass the
1024
- absolute path explicitly.
1021
+ file_path: (optional) explicit WAV path. If omitted, the bridge
1022
+ resolves it automatically via ``get_simpler_file_path``
1023
+ (v1.23.3+). Pass explicitly only when running against an .amxd
1024
+ freeze that predates the case (returns the bridge error string
1025
+ in that case so the caller knows to re-freeze).
1025
1026
 
1026
1027
  Returns: dict with ``slices`` list. Each slice entry has:
1027
1028
  index, frame, seconds, midi_pitch (36+index), label, peak, rms,
@@ -1045,25 +1046,54 @@ async def classify_simpler_slices(
1045
1046
  if enriched is None:
1046
1047
  return {"error": "Bridge returned no slice data"}
1047
1048
 
1048
- # 2. Resolve file path
1049
+ # 2. Resolve file path via Remote Script TCP path (v1.23.3+ — closes
1050
+ # the v1.12 follow-up). Reads ``device.sample.file_path`` directly
1051
+ # via Python LOM, more reliable than the M4L bridge UDP round-trip
1052
+ # (which surfaced a chunked-response correlation issue under live
1053
+ # testing). The bridge case `get_simpler_file_path` is still
1054
+ # registered as a forward-compat fallback for environments where
1055
+ # Remote Script is unavailable.
1049
1056
  wav_path = file_path
1057
+ resolve_error: str | None = None
1050
1058
  if not wav_path:
1051
- try:
1052
- file_info = await bridge.send_command(
1053
- "get_simpler_file_path", track_index, device_index
1054
- )
1055
- if isinstance(file_info, dict):
1056
- wav_path = file_info.get("file_path")
1057
- except Exception: # noqa: BLE001 — bridge command may not exist yet
1058
- wav_path = None
1059
+ ableton = ctx.request_context.lifespan_context.get("ableton")
1060
+ if ableton is not None:
1061
+ try:
1062
+ rs_resp = ableton.send_command(
1063
+ "get_simpler_file_path",
1064
+ {"track_index": track_index, "device_index": device_index},
1065
+ )
1066
+ if isinstance(rs_resp, dict):
1067
+ wav_path = rs_resp.get("file_path")
1068
+ resolve_error = rs_resp.get("error")
1069
+ except Exception as exc: # noqa: BLE001
1070
+ resolve_error = f"remote_script unreachable: {exc}"
1071
+ wav_path = None
1072
+ else:
1073
+ resolve_error = "no ableton TCP connection"
1074
+
1075
+ # Fallback: try the M4L bridge if Remote Script didn't yield a path.
1076
+ # Useful if a stale Remote Script install lacks the new handler
1077
+ # (the user can call reload_handlers to refresh without restart).
1078
+ if not wav_path:
1079
+ try:
1080
+ bridge_resp = await bridge.send_command(
1081
+ "get_simpler_file_path", track_index, device_index
1082
+ )
1083
+ if isinstance(bridge_resp, dict):
1084
+ bridge_path = bridge_resp.get("file_path")
1085
+ if bridge_path:
1086
+ wav_path = bridge_path
1087
+ resolve_error = None
1088
+ except Exception: # noqa: BLE001 — defensive
1089
+ pass
1059
1090
 
1060
1091
  if not wav_path:
1061
1092
  return {
1062
1093
  **enriched,
1063
1094
  "error": (
1064
- "No file_path available — pass file_path= explicitly. "
1065
- "Bridge-based lookup for Simpler sample paths is a v1.12 "
1066
- "follow-up."
1095
+ resolve_error
1096
+ or "No file_path available pass file_path= explicitly."
1067
1097
  ),
1068
1098
  }
1069
1099
 
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "livepilot",
3
- "version": "1.23.2",
3
+ "version": "1.23.3",
4
4
  "mcpName": "io.github.dreamrec/livepilot",
5
- "description": "Agentic production system for Ableton Live 12 433 tools, 53 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",
5
+ "description": "Agentic production system for Ableton Live 12 \u2014 433 tools, 53 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",
7
7
  "license": "BSL-1.1",
8
8
  "type": "commonjs",
@@ -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.2"
8
+ __version__ = "1.23.3"
9
9
 
10
10
  from _Framework.ControlSurface import ControlSurface
11
11
  from . import router
@@ -618,11 +618,20 @@ def insert_rack_chain(song, params):
618
618
  song.end_undo_step()
619
619
 
620
620
  chain_count = len(list(device.chains))
621
+ # BUG-2026-04-25: callers (notably add_drum_rack_pad) expect
622
+ # `chain_index` in the response so they can target the new chain in
623
+ # subsequent set_drum_chain_note + insert_device calls. Without it,
624
+ # the caller falls back to chain_index=0 and overwrites/clobbers the
625
+ # first chain. For position=-1 (append), the new chain lives at
626
+ # chain_count - 1; for explicit position N (0-indexed), the new
627
+ # chain lives at N (insert_chain shifts later chains right).
628
+ new_chain_index = position if 0 <= position < chain_count else chain_count - 1
621
629
  result = {
622
630
  "inserted": True,
623
631
  "track_index": track_index,
624
632
  "device_index": device_index,
625
633
  "chain_count": chain_count,
634
+ "chain_index": new_chain_index,
626
635
  }
627
636
  if assigned_note is not None:
628
637
  result["assigned_pad_note"] = assigned_note
@@ -184,3 +184,101 @@ def replace_sample_native(song, params):
184
184
  "method": "native_12_4",
185
185
  "live_version": version_string(),
186
186
  }
187
+
188
+
189
+ @register("get_simpler_file_path")
190
+ def get_simpler_file_path(song, params):
191
+ """Read the absolute file path of a Simpler's currently-loaded sample.
192
+
193
+ Closes the v1.12 follow-up that left ``classify_simpler_slices`` unable
194
+ to auto-resolve the WAV path: previously the call routed through the
195
+ M4L bridge ``get_simpler_file_path`` case (added in v1.23.3 JS) but
196
+ Live's M4L UDP response correlation produced wrong-data on the second
197
+ successive bridge call (a chunked-response edge case under
198
+ investigation in the bridge wire protocol). The Remote Script reads
199
+ ``device.sample.file_path`` directly via Python LOM — no UDP, no Max
200
+ JS, no chunk reassembly, no ambiguity.
201
+
202
+ params dict keys:
203
+ track_index (int): 0-based index into song.tracks.
204
+ device_index (int): 0-based index into track.devices.
205
+ chain_index (int|None): optional, for nested Drum Rack chains.
206
+ nested_device_index (int|None): optional, position inside the chain.
207
+
208
+ Returns on success:
209
+ file_path (str): absolute filesystem path of the loaded sample.
210
+ track_index, device_index, chain_index, nested_device_index: echoed.
211
+ name (str): Simpler device name (typically the sample filename).
212
+
213
+ Returns on error:
214
+ error (str): human-readable message.
215
+ code (str): STATE_ERROR | INDEX_ERROR | INVALID_PARAM.
216
+ """
217
+ try:
218
+ track_index = int(params["track_index"])
219
+ device_index = int(params["device_index"])
220
+ except (KeyError, TypeError, ValueError) as exc:
221
+ return {
222
+ "error": "Invalid params: " + str(exc),
223
+ "code": "INVALID_PARAM",
224
+ }
225
+
226
+ chain_index = params.get("chain_index")
227
+ nested_device_index = params.get("nested_device_index")
228
+ if chain_index is not None:
229
+ try:
230
+ chain_index = int(chain_index)
231
+ except (TypeError, ValueError):
232
+ return {
233
+ "error": "chain_index must be an integer if provided",
234
+ "code": "INVALID_PARAM",
235
+ }
236
+ if nested_device_index is not None:
237
+ try:
238
+ nested_device_index = int(nested_device_index)
239
+ except (TypeError, ValueError):
240
+ return {
241
+ "error": "nested_device_index must be an integer if provided",
242
+ "code": "INVALID_PARAM",
243
+ }
244
+
245
+ device, err = _resolve_simpler_device(
246
+ song, track_index, device_index, chain_index, nested_device_index,
247
+ )
248
+ if err is not None:
249
+ return err
250
+
251
+ class_name = getattr(device, "class_name", "")
252
+ if class_name != "OriginalSimpler":
253
+ return {
254
+ "error": "Device at resolved path is " + class_name + ", not Simpler",
255
+ "code": "INVALID_PARAM",
256
+ }
257
+
258
+ sample = getattr(device, "sample", None)
259
+ if sample is None:
260
+ return {
261
+ "error": "Simpler has no sample loaded (device.sample is None)",
262
+ "code": "STATE_ERROR",
263
+ }
264
+
265
+ file_path = getattr(sample, "file_path", None)
266
+ if not file_path:
267
+ return {
268
+ "error": (
269
+ "Simpler.sample.file_path is empty — sample may be embedded "
270
+ "in the Set or otherwise lacks a filesystem path."
271
+ ),
272
+ "code": "STATE_ERROR",
273
+ }
274
+
275
+ name = getattr(device, "name", "")
276
+
277
+ return {
278
+ "file_path": str(file_path),
279
+ "track_index": track_index,
280
+ "device_index": device_index,
281
+ "chain_index": chain_index,
282
+ "nested_device_index": nested_device_index,
283
+ "name": str(name),
284
+ }
package/server.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
3
3
  "name": "io.github.dreamrec/livepilot",
4
- "description": "433-tool agentic MCP production system for Ableton Live 12 53 domains, 44 semantic moves, device atlas (5264 devices), Splice intelligence (gRPC + GraphQL), 9-band spectral perception auto-loaded, Creative Director skill, technique memory, 12 creative engines",
4
+ "description": "433-tool agentic MCP production system for Ableton Live 12 \u2014 53 domains, 44 semantic moves, device atlas (5264 devices), Splice intelligence (gRPC + GraphQL), 9-band spectral perception auto-loaded, Creative Director skill, technique memory, 12 creative engines",
5
5
  "repository": {
6
6
  "url": "https://github.com/dreamrec/LivePilot",
7
7
  "source": "github"
8
8
  },
9
- "version": "1.23.2",
9
+ "version": "1.23.3",
10
10
  "packages": [
11
11
  {
12
12
  "registryType": "npm",