livepilot 1.9.5 → 1.9.7

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.
@@ -10,7 +10,7 @@
10
10
  {
11
11
  "name": "livepilot",
12
12
  "description": "Agentic production system for Ableton Live 12 — 178 tools, 17 domains, device atlas, spectral perception, technique memory, neo-Riemannian harmony, Euclidean rhythm, species counterpoint, MIDI I/O",
13
- "version": "1.9.5",
13
+ "version": "1.9.7",
14
14
  "author": {
15
15
  "name": "Pilot Studio"
16
16
  },
package/AGENTS.md CHANGED
@@ -1,4 +1,4 @@
1
- # LivePilot v1.9.5 — Ableton Live 12
1
+ # LivePilot v1.9.7 — Ableton Live 12
2
2
 
3
3
  ## Project
4
4
  - **Repo:** This directory (LivePilot)
package/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.9.7 — Safe automation fallback + correct clip length reporting (March 2026)
4
+
5
+ - Fix(P1): set_arrangement_automation places replacement BEFORE deleting original — no data loss if placement fails
6
+ - Fix(P2): get_arrangement_clips reports timeline length (not loop span) as length/end_time; loop info as separate fields
7
+ - Reverted the effective-length mangling that misreported looped clip sizes
8
+
9
+ ## 1.9.6 — Arrangement clip identification + expression data (March 2026)
10
+
11
+ - Fix(P1): create_arrangement_clip now identifies new clips by object identity, not position match — prevents mutating pre-existing overlapping clips
12
+ - Fix(P2): set_arrangement_automation fallback preserves probability, velocity_deviation, release_velocity when rebuilding notes
13
+ - Fix(P2): get_arrangement_clips effective length uses loop_end - loop_start (not just loop_end)
14
+
3
15
  ## 1.9.5 — TCP Retry Fix + Arrangement Automation Fix (March 2026)
4
16
 
5
17
  - Fix(P1): disconnect() now clears _recv_buf — prevents partial JSON corruption on TCP retry
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "livepilot",
3
- "version": "1.9.5",
3
+ "version": "1.9.7",
4
4
  "description": "Agentic production system for Ableton Live 12 — 178 tools, 17 domains, device atlas, spectral perception, technique memory, neo-Riemannian harmony, Euclidean rhythm, species counterpoint, MIDI I/O",
5
5
  "author": {
6
6
  "name": "Pilot Studio"
@@ -1,4 +1,4 @@
1
- # LivePilot v1.9.5 — Architecture & Tool Reference
1
+ # LivePilot v1.9.7 — Architecture & Tool Reference
2
2
 
3
3
  Agentic production system for Ableton Live 12. 178 tools across 17 domains. Device atlas (280+ devices), spectral perception (M4L analyzer), technique memory, automation intelligence (16 curve types, 15 recipes), music theory (Krumhansl-Schmuckler, species counterpoint), generative algorithms (Euclidean rhythm, tintinnabuli, phase shift, additive process), neo-Riemannian harmony (PRL transforms, Tonnetz), MIDI file I/O.
4
4
 
@@ -83,7 +83,7 @@ function anything() {
83
83
  function dispatch(cmd, args) {
84
84
  switch(cmd) {
85
85
  case "ping":
86
- send_response({"ok": true, "version": "1.9.5"});
86
+ send_response({"ok": true, "version": "1.9.7"});
87
87
  break;
88
88
  case "get_params":
89
89
  cmd_get_params(args);
@@ -1,2 +1,2 @@
1
1
  """LivePilot MCP Server — bridges MCP protocol to Ableton Live."""
2
- __version__ = "1.9.5"
2
+ __version__ = "1.9.7"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "livepilot",
3
- "version": "1.9.5",
3
+ "version": "1.9.7",
4
4
  "mcpName": "io.github.dreamrec/livepilot",
5
5
  "description": "Agentic production system for Ableton Live 12 — 178 tools, 17 domains, device atlas, spectral perception, technique memory, neo-Riemannian harmony, Euclidean rhythm, species counterpoint, MIDI I/O",
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.9.5"
8
+ __version__ = "1.9.7"
9
9
 
10
10
  from _Framework.ControlSurface import ControlSurface
11
11
  from .server import LivePilotServer
@@ -13,25 +13,24 @@ def get_arrangement_clips(song, params):
13
13
  track = get_track(song, track_index)
14
14
  clips = []
15
15
  for i, clip in enumerate(track.arrangement_clips):
16
- timeline_length = clip.length
17
- # Report effective length based on loop_end if looping is active
18
- # (arrangement clips may have been trimmed by create_arrangement_clip)
19
- effective_length = timeline_length
20
- try:
21
- if clip.looping and clip.loop_end < timeline_length:
22
- effective_length = clip.loop_end
23
- except (AttributeError, RuntimeError):
24
- pass
25
- clips.append({
16
+ info = {
26
17
  "index": i,
27
18
  "name": clip.name,
28
19
  "start_time": clip.start_time,
29
- "end_time": clip.start_time + effective_length,
30
- "length": effective_length,
31
- "timeline_length": timeline_length,
20
+ "end_time": clip.start_time + clip.length,
21
+ "length": clip.length,
32
22
  "color_index": clip.color_index,
33
23
  "is_audio_clip": clip.is_audio_clip,
34
- })
24
+ }
25
+ # Add loop info if available
26
+ try:
27
+ if clip.looping:
28
+ info["looping"] = True
29
+ info["loop_start"] = clip.loop_start
30
+ info["loop_end"] = clip.loop_end
31
+ except (AttributeError, RuntimeError):
32
+ pass
33
+ clips.append(info)
35
34
  return {"track_index": track_index, "clips": clips}
36
35
 
37
36
 
@@ -86,35 +85,49 @@ def create_arrangement_clip(song, params):
86
85
  first_clip_index = None
87
86
 
88
87
  while pos < end_pos:
88
+ # Snapshot clip IDs before duplication to identify the new one
89
+ old_ids = set(id(c) for c in track.arrangement_clips)
90
+
89
91
  track.duplicate_clip_to_arrangement(source_clip, pos)
90
92
 
91
- # Find and configure the newly placed clip
93
+ # Find the NEW clip (not in old_ids) at the target position
92
94
  arr_clips = list(track.arrangement_clips)
95
+ new_clip = None
96
+ new_clip_idx = None
93
97
  for i, c in enumerate(arr_clips):
94
- if abs(c.start_time - pos) < 0.01:
95
- if first_clip_index is None:
96
- first_clip_index = i
97
- if name:
98
- c.name = name
99
- if color_index is not None:
100
- c.color_index = int(color_index)
101
-
102
- # When loop_length < source_length, set the internal
103
- # loop region so only loop_length beats of content play.
104
- # Arrangement clip timeline length is read-only in the
105
- # LOM, but overlapping clips are handled by Ableton
106
- # (later clips take priority), so playback is correct.
107
- remaining = end_pos - pos
108
- target_len = min(loop_length, remaining)
109
- if target_len < source_length:
110
- try:
111
- c.looping = True
112
- c.loop_start = 0.0
113
- c.loop_end = target_len
114
- except (AttributeError, RuntimeError):
115
- pass
98
+ if id(c) not in old_ids and abs(c.start_time - pos) < 0.01:
99
+ new_clip = c
100
+ new_clip_idx = i
116
101
  break
117
102
 
103
+ # Fallback: if id-based detection fails, match by position
104
+ if new_clip is None:
105
+ for i, c in enumerate(arr_clips):
106
+ if abs(c.start_time - pos) < 0.01:
107
+ new_clip = c
108
+ new_clip_idx = i
109
+ break
110
+
111
+ if new_clip is not None:
112
+ if first_clip_index is None:
113
+ first_clip_index = new_clip_idx
114
+ if name:
115
+ new_clip.name = name
116
+ if color_index is not None:
117
+ new_clip.color_index = int(color_index)
118
+
119
+ # When loop_length < source_length, set the internal
120
+ # loop region so only loop_length beats of content play.
121
+ remaining = end_pos - pos
122
+ target_len = min(loop_length, remaining)
123
+ if target_len < source_length:
124
+ try:
125
+ new_clip.looping = True
126
+ new_clip.loop_start = 0.0
127
+ new_clip.loop_end = target_len
128
+ except (AttributeError, RuntimeError):
129
+ pass
130
+
118
131
  clip_count += 1
119
132
  pos += loop_length
120
133
 
@@ -574,21 +587,34 @@ def set_arrangement_automation(song, params):
574
587
  import Live
575
588
  note_specs = []
576
589
  for note in orig_notes:
577
- spec = Live.Clip.MidiNoteSpecification(
590
+ kwargs = dict(
578
591
  pitch=note.pitch,
579
592
  start_time=note.start_time,
580
593
  duration=note.duration,
581
594
  velocity=note.velocity,
582
595
  mute=note.mute,
583
596
  )
597
+ # Preserve per-note expression data
598
+ if hasattr(note, 'probability') and note.probability is not None:
599
+ kwargs['probability'] = note.probability
600
+ if hasattr(note, 'velocity_deviation') and note.velocity_deviation is not None:
601
+ kwargs['velocity_deviation'] = note.velocity_deviation
602
+ if hasattr(note, 'release_velocity') and note.release_velocity is not None:
603
+ kwargs['release_velocity'] = note.release_velocity
604
+ spec = Live.Clip.MidiNoteSpecification(**kwargs)
584
605
  note_specs.append(spec)
585
606
  if note_specs:
586
607
  temp_clip.add_new_notes(tuple(note_specs))
587
608
  except Exception:
588
609
  pass # Non-MIDI clips or errors — automation-only is still valid
589
610
 
590
- # Delete the original arrangement clip BEFORE placing the replacement
591
- # to avoid creating a second overlapping clip at the same position.
611
+ # Place the session clip (with automation + notes) into arrangement.
612
+ # Do this BEFORE deleting the original if placement fails, the
613
+ # original clip is preserved (no data loss on partial failure).
614
+ track.duplicate_clip_to_arrangement(temp_clip, arr_start)
615
+
616
+ # Placement succeeded — now safe to remove the original clip
617
+ # to avoid a second overlapping clip at the same position.
592
618
  try:
593
619
  clip.delete_clip()
594
620
  except (AttributeError, RuntimeError):
@@ -596,9 +622,6 @@ def set_arrangement_automation(song, params):
596
622
  # in that case we accept the overlap as a known limitation.
597
623
  pass
598
624
 
599
- # Place the session clip (with automation + notes) into arrangement
600
- track.duplicate_clip_to_arrangement(temp_clip, arr_start)
601
-
602
625
  # Clean up the temporary session clip
603
626
  slot.delete_clip()
604
627