livepilot 1.9.4 → 1.9.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.
@@ -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.4",
13
+ "version": "1.9.6",
14
14
  "author": {
15
15
  "name": "Pilot Studio"
16
16
  },
package/AGENTS.md CHANGED
@@ -1,4 +1,4 @@
1
- # LivePilot v1.9.4 — Ableton Live 12
1
+ # LivePilot v1.9.6 — Ableton Live 12
2
2
 
3
3
  ## Project
4
4
  - **Repo:** This directory (LivePilot)
package/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.9.6 — Arrangement clip identification + expression data (March 2026)
4
+
5
+ - Fix(P1): create_arrangement_clip now identifies new clips by object identity, not position match — prevents mutating pre-existing overlapping clips
6
+ - Fix(P2): set_arrangement_automation fallback preserves probability, velocity_deviation, release_velocity when rebuilding notes
7
+ - Fix(P2): get_arrangement_clips effective length uses loop_end - loop_start (not just loop_end)
8
+
9
+ ## 1.9.5 — TCP Retry Fix + Arrangement Automation Fix (March 2026)
10
+
11
+ - Fix(P1): disconnect() now clears _recv_buf — prevents partial JSON corruption on TCP retry
12
+ - Fix(P1): set_arrangement_automation fallback copies notes + deletes original clip to avoid silent duplication
13
+ - Fix(P2): get_arrangement_clips reports effective length based on loop_end, not raw timeline length
14
+ - 2 new connection tests for recv_buf corruption
15
+ - 257 tests passing
16
+
3
17
  ## 1.9.4 — Doc Sync + M4L Analyzer Fix + Full Validation (March 2026)
4
18
 
5
19
  **178 tools, all validated live in Ableton. M4L analyzer fully working.**
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "livepilot",
3
- "version": "1.9.4",
3
+ "version": "1.9.6",
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.4 — Architecture & Tool Reference
1
+ # LivePilot v1.9.6 — 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.4"});
86
+ send_response({"ok": true, "version": "1.9.6"});
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.4"
2
+ __version__ = "1.9.6"
@@ -84,13 +84,14 @@ class AbletonConnection:
84
84
  ) from exc
85
85
 
86
86
  def disconnect(self) -> None:
87
- """Close the TCP connection."""
87
+ """Close the TCP connection and discard any partial receive buffer."""
88
88
  if self._socket is not None:
89
89
  try:
90
90
  self._socket.close()
91
91
  except OSError:
92
92
  pass
93
93
  self._socket = None
94
+ self._recv_buf = b""
94
95
 
95
96
  def is_connected(self) -> bool:
96
97
  """Return True if a socket is currently held."""
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "livepilot",
3
- "version": "1.9.4",
3
+ "version": "1.9.6",
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.4"
8
+ __version__ = "1.9.6"
9
9
 
10
10
  from _Framework.ControlSurface import ControlSurface
11
11
  from .server import LivePilotServer
@@ -13,12 +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:
22
+ loop_len = clip.loop_end - clip.loop_start
23
+ if 0 < loop_len < timeline_length:
24
+ effective_length = loop_len
25
+ except (AttributeError, RuntimeError):
26
+ pass
16
27
  clips.append({
17
28
  "index": i,
18
29
  "name": clip.name,
19
30
  "start_time": clip.start_time,
20
- "end_time": clip.start_time + clip.length,
21
- "length": clip.length,
31
+ "end_time": clip.start_time + effective_length,
32
+ "length": effective_length,
33
+ "timeline_length": timeline_length,
22
34
  "color_index": clip.color_index,
23
35
  "is_audio_clip": clip.is_audio_clip,
24
36
  })
@@ -76,35 +88,49 @@ def create_arrangement_clip(song, params):
76
88
  first_clip_index = None
77
89
 
78
90
  while pos < end_pos:
91
+ # Snapshot clip IDs before duplication to identify the new one
92
+ old_ids = set(id(c) for c in track.arrangement_clips)
93
+
79
94
  track.duplicate_clip_to_arrangement(source_clip, pos)
80
95
 
81
- # Find and configure the newly placed clip
96
+ # Find the NEW clip (not in old_ids) at the target position
82
97
  arr_clips = list(track.arrangement_clips)
98
+ new_clip = None
99
+ new_clip_idx = None
83
100
  for i, c in enumerate(arr_clips):
84
- if abs(c.start_time - pos) < 0.01:
85
- if first_clip_index is None:
86
- first_clip_index = i
87
- if name:
88
- c.name = name
89
- if color_index is not None:
90
- c.color_index = int(color_index)
91
-
92
- # When loop_length < source_length, set the internal
93
- # loop region so only loop_length beats of content play.
94
- # Arrangement clip timeline length is read-only in the
95
- # LOM, but overlapping clips are handled by Ableton
96
- # (later clips take priority), so playback is correct.
97
- remaining = end_pos - pos
98
- target_len = min(loop_length, remaining)
99
- if target_len < source_length:
100
- try:
101
- c.looping = True
102
- c.loop_start = 0.0
103
- c.loop_end = target_len
104
- except (AttributeError, RuntimeError):
105
- pass
101
+ if id(c) not in old_ids and abs(c.start_time - pos) < 0.01:
102
+ new_clip = c
103
+ new_clip_idx = i
106
104
  break
107
105
 
106
+ # Fallback: if id-based detection fails, match by position
107
+ if new_clip is None:
108
+ for i, c in enumerate(arr_clips):
109
+ if abs(c.start_time - pos) < 0.01:
110
+ new_clip = c
111
+ new_clip_idx = i
112
+ break
113
+
114
+ if new_clip is not None:
115
+ if first_clip_index is None:
116
+ first_clip_index = new_clip_idx
117
+ if name:
118
+ new_clip.name = name
119
+ if color_index is not None:
120
+ new_clip.color_index = int(color_index)
121
+
122
+ # When loop_length < source_length, set the internal
123
+ # loop region so only loop_length beats of content play.
124
+ remaining = end_pos - pos
125
+ target_len = min(loop_length, remaining)
126
+ if target_len < source_length:
127
+ try:
128
+ new_clip.looping = True
129
+ new_clip.loop_start = 0.0
130
+ new_clip.loop_end = target_len
131
+ except (AttributeError, RuntimeError):
132
+ pass
133
+
108
134
  clip_count += 1
109
135
  pos += loop_length
110
136
 
@@ -557,7 +583,44 @@ def set_arrangement_automation(song, params):
557
583
  temp_envelope.insert_step(time, duration, value)
558
584
  points_written += 1
559
585
 
560
- # Duplicate session clip to arrangement at the same position
586
+ # Copy notes from original arrangement clip to the temp session clip
587
+ # so the replacement clip has the same musical content.
588
+ try:
589
+ orig_notes = clip.get_notes_extended(0, 128, 0.0, arr_length + 1.0)
590
+ import Live
591
+ note_specs = []
592
+ for note in orig_notes:
593
+ kwargs = dict(
594
+ pitch=note.pitch,
595
+ start_time=note.start_time,
596
+ duration=note.duration,
597
+ velocity=note.velocity,
598
+ mute=note.mute,
599
+ )
600
+ # Preserve per-note expression data
601
+ if hasattr(note, 'probability') and note.probability is not None:
602
+ kwargs['probability'] = note.probability
603
+ if hasattr(note, 'velocity_deviation') and note.velocity_deviation is not None:
604
+ kwargs['velocity_deviation'] = note.velocity_deviation
605
+ if hasattr(note, 'release_velocity') and note.release_velocity is not None:
606
+ kwargs['release_velocity'] = note.release_velocity
607
+ spec = Live.Clip.MidiNoteSpecification(**kwargs)
608
+ note_specs.append(spec)
609
+ if note_specs:
610
+ temp_clip.add_new_notes(tuple(note_specs))
611
+ except Exception:
612
+ pass # Non-MIDI clips or errors — automation-only is still valid
613
+
614
+ # Delete the original arrangement clip BEFORE placing the replacement
615
+ # to avoid creating a second overlapping clip at the same position.
616
+ try:
617
+ clip.delete_clip()
618
+ except (AttributeError, RuntimeError):
619
+ # delete_clip may not exist on arrangement clips in all versions;
620
+ # in that case we accept the overlap as a known limitation.
621
+ pass
622
+
623
+ # Place the session clip (with automation + notes) into arrangement
561
624
  track.duplicate_clip_to_arrangement(temp_clip, arr_start)
562
625
 
563
626
  # Clean up the temporary session clip