livepilot 1.10.7 → 1.10.8

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.
Files changed (122) hide show
  1. package/CHANGELOG.md +126 -0
  2. package/README.md +11 -9
  3. package/bin/livepilot.js +146 -28
  4. package/installer/install.js +117 -11
  5. package/m4l_device/LivePilot_Analyzer.amxd +0 -0
  6. package/m4l_device/livepilot_bridge.js +1 -1
  7. package/mcp_server/__init__.py +1 -1
  8. package/mcp_server/atlas/__init__.py +39 -7
  9. package/mcp_server/atlas/tools.py +56 -15
  10. package/mcp_server/composer/layer_planner.py +27 -0
  11. package/mcp_server/composer/prompt_parser.py +15 -6
  12. package/mcp_server/connection.py +11 -3
  13. package/mcp_server/corpus/__init__.py +14 -4
  14. package/mcp_server/m4l_bridge.py +48 -7
  15. package/mcp_server/runtime/execution_router.py +16 -2
  16. package/mcp_server/runtime/remote_commands.py +6 -0
  17. package/mcp_server/sample_engine/models.py +22 -3
  18. package/mcp_server/semantic_moves/__init__.py +1 -0
  19. package/mcp_server/semantic_moves/compiler.py +9 -1
  20. package/mcp_server/semantic_moves/device_creation_compilers.py +47 -0
  21. package/mcp_server/semantic_moves/mix_compilers.py +170 -0
  22. package/mcp_server/semantic_moves/mix_moves.py +1 -1
  23. package/mcp_server/semantic_moves/models.py +5 -0
  24. package/mcp_server/semantic_moves/tools.py +15 -4
  25. package/mcp_server/server.py +7 -3
  26. package/mcp_server/services/singletons.py +68 -0
  27. package/mcp_server/splice_client/client.py +29 -8
  28. package/mcp_server/tools/analyzer.py +7 -6
  29. package/mcp_server/tools/clips.py +1 -1
  30. package/mcp_server/tools/midi_io.py +10 -0
  31. package/mcp_server/tools/tracks.py +1 -1
  32. package/mcp_server/tools/transport.py +1 -1
  33. package/mcp_server/translation_engine/tools.py +8 -4
  34. package/package.json +25 -3
  35. package/remote_script/LivePilot/__init__.py +29 -9
  36. package/remote_script/LivePilot/arrangement.py +12 -2
  37. package/remote_script/LivePilot/browser.py +16 -6
  38. package/remote_script/LivePilot/devices.py +10 -5
  39. package/remote_script/LivePilot/notes.py +13 -2
  40. package/remote_script/LivePilot/server.py +51 -13
  41. package/remote_script/LivePilot/version_detect.py +7 -4
  42. package/server.json +20 -0
  43. package/.claude-plugin/marketplace.json +0 -21
  44. package/.mcp.json.disabled +0 -9
  45. package/.mcpbignore +0 -60
  46. package/AGENTS.md +0 -46
  47. package/BUGS.md +0 -1570
  48. package/CODE_OF_CONDUCT.md +0 -27
  49. package/CONTRIBUTING.md +0 -131
  50. package/SECURITY.md +0 -48
  51. package/livepilot/.Codex-plugin/plugin.json +0 -8
  52. package/livepilot/.claude-plugin/plugin.json +0 -8
  53. package/livepilot/agents/livepilot-producer/AGENT.md +0 -313
  54. package/livepilot/commands/arrange.md +0 -47
  55. package/livepilot/commands/beat.md +0 -77
  56. package/livepilot/commands/evaluate.md +0 -49
  57. package/livepilot/commands/memory.md +0 -22
  58. package/livepilot/commands/mix.md +0 -44
  59. package/livepilot/commands/perform.md +0 -42
  60. package/livepilot/commands/session.md +0 -13
  61. package/livepilot/commands/sounddesign.md +0 -43
  62. package/livepilot/skills/livepilot-arrangement/SKILL.md +0 -155
  63. package/livepilot/skills/livepilot-composition-engine/SKILL.md +0 -107
  64. package/livepilot/skills/livepilot-composition-engine/references/form-patterns.md +0 -97
  65. package/livepilot/skills/livepilot-composition-engine/references/transition-archetypes.md +0 -102
  66. package/livepilot/skills/livepilot-core/SKILL.md +0 -184
  67. package/livepilot/skills/livepilot-core/references/ableton-workflow-patterns.md +0 -831
  68. package/livepilot/skills/livepilot-core/references/automation-atlas.md +0 -272
  69. package/livepilot/skills/livepilot-core/references/device-atlas/00-index.md +0 -110
  70. package/livepilot/skills/livepilot-core/references/device-atlas/distortion-and-character.md +0 -687
  71. package/livepilot/skills/livepilot-core/references/device-atlas/drums-and-percussion.md +0 -753
  72. package/livepilot/skills/livepilot-core/references/device-atlas/dynamics-and-punch.md +0 -525
  73. package/livepilot/skills/livepilot-core/references/device-atlas/eq-and-filtering.md +0 -402
  74. package/livepilot/skills/livepilot-core/references/device-atlas/midi-tools.md +0 -963
  75. package/livepilot/skills/livepilot-core/references/device-atlas/movement-and-modulation.md +0 -874
  76. package/livepilot/skills/livepilot-core/references/device-atlas/space-and-depth.md +0 -571
  77. package/livepilot/skills/livepilot-core/references/device-atlas/spectral-and-weird.md +0 -714
  78. package/livepilot/skills/livepilot-core/references/device-atlas/synths-native.md +0 -953
  79. package/livepilot/skills/livepilot-core/references/device-knowledge/00-index.md +0 -34
  80. package/livepilot/skills/livepilot-core/references/device-knowledge/automation-as-music.md +0 -204
  81. package/livepilot/skills/livepilot-core/references/device-knowledge/chains-genre.md +0 -173
  82. package/livepilot/skills/livepilot-core/references/device-knowledge/creative-thinking.md +0 -211
  83. package/livepilot/skills/livepilot-core/references/device-knowledge/effects-distortion.md +0 -188
  84. package/livepilot/skills/livepilot-core/references/device-knowledge/effects-space.md +0 -162
  85. package/livepilot/skills/livepilot-core/references/device-knowledge/effects-spectral.md +0 -229
  86. package/livepilot/skills/livepilot-core/references/device-knowledge/instruments-synths.md +0 -243
  87. package/livepilot/skills/livepilot-core/references/m4l-devices.md +0 -352
  88. package/livepilot/skills/livepilot-core/references/memory-guide.md +0 -107
  89. package/livepilot/skills/livepilot-core/references/midi-recipes.md +0 -402
  90. package/livepilot/skills/livepilot-core/references/mixing-patterns.md +0 -578
  91. package/livepilot/skills/livepilot-core/references/overview.md +0 -290
  92. package/livepilot/skills/livepilot-core/references/sample-manipulation.md +0 -724
  93. package/livepilot/skills/livepilot-core/references/sound-design-deep.md +0 -140
  94. package/livepilot/skills/livepilot-core/references/sound-design.md +0 -393
  95. package/livepilot/skills/livepilot-devices/SKILL.md +0 -169
  96. package/livepilot/skills/livepilot-evaluation/SKILL.md +0 -156
  97. package/livepilot/skills/livepilot-evaluation/references/capability-modes.md +0 -118
  98. package/livepilot/skills/livepilot-evaluation/references/evaluation-contracts.md +0 -121
  99. package/livepilot/skills/livepilot-evaluation/references/memory-promotion.md +0 -110
  100. package/livepilot/skills/livepilot-mix-engine/SKILL.md +0 -123
  101. package/livepilot/skills/livepilot-mix-engine/references/mix-critics.md +0 -143
  102. package/livepilot/skills/livepilot-mix-engine/references/mix-moves.md +0 -105
  103. package/livepilot/skills/livepilot-mixing/SKILL.md +0 -157
  104. package/livepilot/skills/livepilot-notes/SKILL.md +0 -130
  105. package/livepilot/skills/livepilot-performance-engine/SKILL.md +0 -122
  106. package/livepilot/skills/livepilot-performance-engine/references/performance-safety.md +0 -98
  107. package/livepilot/skills/livepilot-release/SKILL.md +0 -130
  108. package/livepilot/skills/livepilot-sample-engine/SKILL.md +0 -105
  109. package/livepilot/skills/livepilot-sample-engine/references/sample-critics.md +0 -87
  110. package/livepilot/skills/livepilot-sample-engine/references/sample-philosophy.md +0 -51
  111. package/livepilot/skills/livepilot-sample-engine/references/sample-techniques.md +0 -131
  112. package/livepilot/skills/livepilot-sound-design-engine/SKILL.md +0 -168
  113. package/livepilot/skills/livepilot-sound-design-engine/references/patch-model.md +0 -119
  114. package/livepilot/skills/livepilot-sound-design-engine/references/sound-design-critics.md +0 -118
  115. package/livepilot/skills/livepilot-wonder/SKILL.md +0 -79
  116. package/m4l_device/LivePilot_Analyzer.amxd.pre-presentation-backup +0 -0
  117. package/m4l_device/LivePilot_Analyzer.maxpat +0 -2705
  118. package/m4l_device/LivePilot_Analyzer.maxproj +0 -53
  119. package/manifest.json +0 -91
  120. package/mcp_server/splice_client/protos/app_pb2.pyi +0 -1153
  121. package/scripts/generate_tool_catalog.py +0 -106
  122. package/scripts/sync_metadata.py +0 -349
@@ -282,6 +282,173 @@ def _compile_reduce_repetition(move: SemanticMove, kernel: dict) -> CompiledPlan
282
282
  )
283
283
 
284
284
 
285
+ def _compile_make_kick_bass_lock(move: SemanticMove, kernel: dict) -> CompiledPlan:
286
+ """Compile 'make_kick_bass_lock': carve space between kick and bass.
287
+
288
+ Strategy: reduce bass level slightly (clears sub for kick), verify both
289
+ tracks remain active. Sidechain compressor insertion is left as a future
290
+ step — it requires device selection + parameter mapping that varies too
291
+ much across projects to hardcode safely.
292
+ """
293
+ steps: list[CompiledStep] = []
294
+ warnings: list[str] = []
295
+ descriptions: list[str] = []
296
+
297
+ bass_tracks = resolvers.find_tracks_by_role(kernel, ["bass"])
298
+ kick_tracks = resolvers.find_tracks_by_role(kernel, ["drums", "percussion"])
299
+
300
+ if not bass_tracks:
301
+ warnings.append("No bass track found — cannot lock kick and bass")
302
+ if not kick_tracks:
303
+ warnings.append("No kick/drum track found — reference track missing")
304
+
305
+ steps.append(CompiledStep(
306
+ tool="get_master_spectrum",
307
+ params={},
308
+ description="Read current sub/low balance before carving",
309
+ verify_after=False,
310
+ ))
311
+
312
+ if bass_tracks:
313
+ bass = bass_tracks[0]
314
+ idx = bass["index"]
315
+ steps.append(CompiledStep(
316
+ tool="set_track_volume",
317
+ params={"track_index": idx, "volume": 0.60},
318
+ description=f"Pull {bass['name']} to 0.60 to clear sub for kick",
319
+ ))
320
+ descriptions.append(f"Pull {bass['name']} to 0.60")
321
+
322
+ steps.append(CompiledStep(
323
+ tool="get_track_meters",
324
+ params={"include_stereo": True},
325
+ description="Verify kick and bass both still producing audio",
326
+ ))
327
+
328
+ return CompiledPlan(
329
+ move_id=move.move_id,
330
+ intent=move.intent,
331
+ steps=steps,
332
+ before_reads=[{"tool": "get_master_spectrum", "params": {}}],
333
+ after_reads=[
334
+ {"tool": "get_master_spectrum", "params": {}},
335
+ {"tool": "get_track_meters", "params": {"include_stereo": True}},
336
+ ],
337
+ risk_level="low",
338
+ summary="; ".join(descriptions) if descriptions else "No kick/bass changes compiled",
339
+ requires_approval=(kernel.get("mode", "improve") != "explore"),
340
+ warnings=warnings,
341
+ )
342
+
343
+
344
+ def _compile_create_buildup_tension(move: SemanticMove, kernel: dict) -> CompiledPlan:
345
+ """Compile 'create_buildup_tension': pull harmony back, raise perc energy.
346
+
347
+ We apply volume moves as the minimal, reversible tension-builder. Filter
348
+ rises and send ramps belong in an automation recipe — we issue a tension
349
+ gesture template step if the gesture engine is available, otherwise fall
350
+ back to direct volume changes only.
351
+ """
352
+ steps: list[CompiledStep] = []
353
+ warnings: list[str] = []
354
+ descriptions: list[str] = []
355
+
356
+ perc_tracks = resolvers.find_tracks_by_role(kernel, ["drums", "percussion"])
357
+ harmony_tracks = resolvers.find_tracks_by_role(kernel, ["chords", "pad"])
358
+
359
+ if not perc_tracks and not harmony_tracks:
360
+ warnings.append("No percussion or harmony tracks found — cannot build tension")
361
+
362
+ # Raise perc for energy
363
+ for pt in perc_tracks[:1]:
364
+ steps.append(CompiledStep(
365
+ tool="set_track_volume",
366
+ params={"track_index": pt["index"], "volume": 0.78},
367
+ description=f"Push {pt['name']} to 0.78 for rising energy",
368
+ ))
369
+ descriptions.append(f"Push {pt['name']} to 0.78")
370
+
371
+ # Pull harmony slightly to amplify perc contrast
372
+ for ht in harmony_tracks[:1]:
373
+ steps.append(CompiledStep(
374
+ tool="set_track_volume",
375
+ params={"track_index": ht["index"], "volume": 0.35},
376
+ description=f"Pull {ht['name']} to 0.35 to create harmonic vacuum before drop",
377
+ ))
378
+ descriptions.append(f"Pull {ht['name']} to 0.35")
379
+
380
+ steps.append(CompiledStep(
381
+ tool="get_track_meters",
382
+ params={"include_stereo": True},
383
+ description="Verify tension steps did not silence any track",
384
+ ))
385
+
386
+ return CompiledPlan(
387
+ move_id=move.move_id,
388
+ intent=move.intent,
389
+ steps=steps,
390
+ before_reads=[{"tool": "get_emotional_arc", "params": {}}],
391
+ after_reads=[
392
+ {"tool": "get_emotional_arc", "params": {}},
393
+ {"tool": "get_track_meters", "params": {"include_stereo": True}},
394
+ ],
395
+ risk_level="medium",
396
+ summary="; ".join(descriptions) if descriptions else "No tracks to ratchet",
397
+ requires_approval=(kernel.get("mode", "improve") != "explore"),
398
+ warnings=warnings,
399
+ )
400
+
401
+
402
+ def _compile_smooth_scene_handoff(move: SemanticMove, kernel: dict) -> CompiledPlan:
403
+ """Compile 'smooth_scene_handoff': reduce master volume briefly around the handoff.
404
+
405
+ Without knowing which two scenes are involved, the compiler can only do a
406
+ conservative energy dip using master volume. A future version should take
407
+ scene indices via kernel.intent_context and apply targeted crossfades.
408
+ """
409
+ steps: list[CompiledStep] = []
410
+ warnings: list[str] = []
411
+ descriptions: list[str] = []
412
+
413
+ # Minimal approach — gentle master dip the agent can reverse easily.
414
+ steps.append(CompiledStep(
415
+ tool="get_master_meters",
416
+ params={},
417
+ description="Record current master level for handoff reference",
418
+ verify_after=False,
419
+ ))
420
+
421
+ steps.append(CompiledStep(
422
+ tool="set_master_volume",
423
+ params={"volume": 0.78},
424
+ description="Gentle master dip for transition",
425
+ ))
426
+ descriptions.append("Master dip to 0.78")
427
+
428
+ steps.append(CompiledStep(
429
+ tool="get_master_meters",
430
+ params={},
431
+ description="Verify master dip applied without clipping",
432
+ ))
433
+
434
+ warnings.append(
435
+ "Scene-aware handoff (from_scene/to_scene) not yet compiled — "
436
+ "this is a conservative energy-dip fallback"
437
+ )
438
+
439
+ return CompiledPlan(
440
+ move_id=move.move_id,
441
+ intent=move.intent,
442
+ steps=steps,
443
+ before_reads=[{"tool": "get_emotional_arc", "params": {}}],
444
+ after_reads=[{"tool": "get_emotional_arc", "params": {}}],
445
+ risk_level="low",
446
+ summary="; ".join(descriptions),
447
+ requires_approval=(kernel.get("mode", "improve") != "explore"),
448
+ warnings=warnings,
449
+ )
450
+
451
+
285
452
  # ── Register all compilers ──────────────────────────────────────────────────
286
453
 
287
454
  register_compiler("make_punchier", _compile_make_punchier)
@@ -289,3 +456,6 @@ register_compiler("tighten_low_end", _compile_tighten_low_end)
289
456
  register_compiler("widen_stereo", _compile_widen_stereo)
290
457
  register_compiler("darken_without_losing_width", _compile_darken_mix)
291
458
  register_compiler("reduce_repetition_fatigue", _compile_reduce_repetition)
459
+ register_compiler("make_kick_bass_lock", _compile_make_kick_bass_lock)
460
+ register_compiler("create_buildup_tension", _compile_create_buildup_tension)
461
+ register_compiler("smooth_scene_handoff", _compile_smooth_scene_handoff)
@@ -92,7 +92,7 @@ REDUCE_REPETITION = SemanticMove(
92
92
  ],
93
93
  verification_plan=[
94
94
  {"tool": "get_track_meters", "check": "all tracks still producing audio", "backend": "remote_command"},
95
- {"tool": "capture_audio", "check": "LRA > 2 LU (dynamic range should increase)", "backend": "mcp_tool"},
95
+ {"tool": "capture_audio", "check": "LRA > 2 LU (dynamic range should increase)", "backend": "bridge_command"},
96
96
  ],
97
97
  )
98
98
 
@@ -24,6 +24,10 @@ class SemanticMove:
24
24
  plan_template: list = field(default_factory=list) # [{tool, params, description}] — static metadata, NOT runtime truth
25
25
  verification_plan: list = field(default_factory=list) # [{tool, check}]
26
26
  confidence: float = 0.7
27
+ # analytical_only: move is intentionally metadata-only — no compiler is
28
+ # expected. Surfaces in discovery/wonder_mode but never executes. Set this
29
+ # to True for moves that are deliberate "hints" rather than orphan-by-bug.
30
+ analytical_only: bool = False
27
31
 
28
32
  def to_dict(self) -> dict:
29
33
  return {
@@ -36,6 +40,7 @@ class SemanticMove:
36
40
  "required_capabilities": self.required_capabilities,
37
41
  "plan_template_steps": len(self.plan_template),
38
42
  "confidence": self.confidence,
43
+ "analytical_only": self.analytical_only,
39
44
  }
40
45
 
41
46
  def to_full_dict(self) -> dict:
@@ -229,10 +229,21 @@ async def apply_semantic_move(
229
229
  # explore mode — execute through the async router
230
230
  from ..runtime.execution_router import execute_plan_steps_async
231
231
 
232
- step_dicts = [
233
- {"tool": step.tool, "params": step.params, "description": step.description}
234
- for step in plan.steps
235
- ]
232
+ # Propagate the optional backend annotation through to the router so a
233
+ # compiler that's certain about a step's backend (e.g. bridge_command for
234
+ # capture_audio) can short-circuit classify_step(). Steps without backend
235
+ # fall back to the classifier as before.
236
+ def _step_to_dict(step):
237
+ d = {
238
+ "tool": step.tool,
239
+ "params": step.params,
240
+ "description": step.description,
241
+ }
242
+ if getattr(step, "backend", None):
243
+ d["backend"] = step.backend
244
+ return d
245
+
246
+ step_dicts = [_step_to_dict(step) for step in plan.steps]
236
247
  bridge = ctx.lifespan_context.get("m4l")
237
248
  mcp_registry = ctx.lifespan_context.get("mcp_dispatch", {})
238
249
  exec_results = await execute_plan_steps_async(
@@ -2,6 +2,7 @@
2
2
 
3
3
  from contextlib import asynccontextmanager
4
4
  import asyncio
5
+ import logging
5
6
  import os
6
7
  import subprocess
7
8
 
@@ -10,6 +11,12 @@ from fastmcp import FastMCP, Context # noqa: F401
10
11
  from .connection import AbletonConnection
11
12
  from .m4l_bridge import SpectralCache, SpectralReceiver, M4LBridge
12
13
 
14
+ # Logger must be defined before any function uses it — several module-level
15
+ # helpers below (e.g. _master_has_livepilot_analyzer) call logger.debug on
16
+ # the import-time code path, so defining logger later raised NameError when
17
+ # those helpers fired from a tool module's module-level init.
18
+ logger = logging.getLogger(__name__)
19
+
13
20
 
14
21
  def _identify_port_holder(port: int) -> str | None:
15
22
  """Identify which process holds the given UDP port (for logging only).
@@ -264,9 +271,6 @@ from .device_forge import tools as device_forge_tools # noqa: F401, E40
264
271
  from .sample_engine import tools as sample_engine_tools # noqa: F401, E402
265
272
  from .atlas import tools as atlas_tools # noqa: F401, E402
266
273
  from .composer import tools as composer_tools # noqa: F401, E402
267
- import logging
268
-
269
- logger = logging.getLogger(__name__)
270
274
 
271
275
  # ---------------------------------------------------------------------------
272
276
  # Schema coercion patch — accept strings for numeric parameters
@@ -0,0 +1,68 @@
1
+ """Thread-safe singleton helpers.
2
+
3
+ The server has several subsystems (atlas, corpus, sample-engine indexes)
4
+ that are loaded lazily into module-level globals via a check-then-set
5
+ pattern. Under FastMCP's async concurrency that pattern races: two
6
+ handlers can both observe ``None`` and both construct the (expensive)
7
+ object. Most of the time the GIL hides the race, but when it doesn't you
8
+ get redundant I/O and, worse, one thread's half-parsed state overwriting
9
+ the other's completed state.
10
+
11
+ This module provides a small helper that wraps a factory in a lock and
12
+ optionally tracks an on-disk mtime for cache invalidation. Use it in
13
+ place of hand-rolled ``_instance = None`` patterns.
14
+ """
15
+ from __future__ import annotations
16
+
17
+ from pathlib import Path
18
+ from threading import Lock
19
+ from typing import Callable, TypeVar
20
+
21
+ T = TypeVar("T")
22
+
23
+
24
+ class Singleton:
25
+ """Lazy, thread-safe singleton with optional mtime-based reload.
26
+
27
+ Example:
28
+ atlas_holder = Singleton(_load_atlas)
29
+
30
+ def get_atlas():
31
+ return atlas_holder.get(reload_if_newer=atlas_path)
32
+
33
+ def on_atlas_rebuild():
34
+ atlas_holder.invalidate()
35
+ """
36
+
37
+ def __init__(self, factory: Callable[[], T]):
38
+ self._factory = factory
39
+ self._instance: T | None = None
40
+ self._mtime: float | None = None
41
+ self._lock = Lock()
42
+
43
+ def get(self, *, reload_if_newer: Path | None = None) -> T:
44
+ with self._lock:
45
+ if self._instance is None:
46
+ self._instance = self._factory()
47
+ if reload_if_newer is not None:
48
+ try:
49
+ self._mtime = reload_if_newer.stat().st_mtime
50
+ except OSError:
51
+ self._mtime = None
52
+ return self._instance
53
+
54
+ if reload_if_newer is not None:
55
+ try:
56
+ current = reload_if_newer.stat().st_mtime
57
+ except OSError:
58
+ return self._instance
59
+ if self._mtime is None or current > self._mtime:
60
+ self._instance = self._factory()
61
+ self._mtime = current
62
+ return self._instance
63
+
64
+ def invalidate(self) -> None:
65
+ """Discard the cached instance. Next .get() will re-run the factory."""
66
+ with self._lock:
67
+ self._instance = None
68
+ self._mtime = None
@@ -27,6 +27,16 @@ _SPLICE_APP_SUPPORT = os.path.expanduser(
27
27
  # Credit safety floor — never drain below this
28
28
  CREDIT_HARD_FLOOR = 5
29
29
 
30
+ # Per-call gRPC timeouts. The previous implementation passed no timeout, so
31
+ # a hung Splice process could block the MCP event loop until gRPC's default
32
+ # (often infinite) deadline fired. Keep generous enough for cold searches
33
+ # but bounded enough that a dead socket fails the tool call, not the server.
34
+ SEARCH_TIMEOUT = 10.0
35
+ INFO_TIMEOUT = 5.0
36
+ CREDITS_TIMEOUT = 5.0
37
+ SYNC_TIMEOUT = 30.0
38
+ DOWNLOAD_TRIGGER_TIMEOUT = 5.0
39
+
30
40
 
31
41
  def _try_import_grpc():
32
42
  """Import grpcio lazily — graceful degradation if not installed."""
@@ -148,7 +158,7 @@ class SpliceGRPCClient:
148
158
  Page=page,
149
159
  Purchased=purchased,
150
160
  )
151
- response = await self.stub.SearchSamples(request)
161
+ response = await self.stub.SearchSamples(request, timeout=SEARCH_TIMEOUT)
152
162
  return self._parse_search_response(response)
153
163
  except Exception as exc:
154
164
  logger.warning(f"Splice search failed: {exc}")
@@ -213,7 +223,8 @@ class SpliceGRPCClient:
213
223
  try:
214
224
  # Trigger download
215
225
  await self.stub.DownloadSample(
216
- pb2.DownloadSampleRequest(FileHash=file_hash)
226
+ pb2.DownloadSampleRequest(FileHash=file_hash),
227
+ timeout=DOWNLOAD_TRIGGER_TIMEOUT,
217
228
  )
218
229
  # Wait for file to appear on disk
219
230
  return await self._wait_for_download(file_hash, timeout)
@@ -226,11 +237,16 @@ class SpliceGRPCClient:
226
237
  ) -> Optional[str]:
227
238
  """Poll SampleInfo until LocalPath is populated."""
228
239
  pb2 = self._pb2
229
- deadline = asyncio.get_event_loop().time() + timeout
230
- while asyncio.get_event_loop().time() < deadline:
240
+ # asyncio.get_event_loop() is deprecated when called inside an
241
+ # already-running coroutine on Python 3.10+. Use get_running_loop()
242
+ # which is the documented replacement.
243
+ loop = asyncio.get_running_loop()
244
+ deadline = loop.time() + timeout
245
+ while loop.time() < deadline:
231
246
  try:
232
247
  response = await self.stub.SampleInfo(
233
- pb2.SampleInfoRequest(FileHash=file_hash)
248
+ pb2.SampleInfoRequest(FileHash=file_hash),
249
+ timeout=INFO_TIMEOUT,
234
250
  )
235
251
  if response.Sample.LocalPath:
236
252
  return response.Sample.LocalPath
@@ -250,7 +266,8 @@ class SpliceGRPCClient:
250
266
  pb2 = self._pb2
251
267
  try:
252
268
  response = await self.stub.SampleInfo(
253
- pb2.SampleInfoRequest(FileHash=file_hash)
269
+ pb2.SampleInfoRequest(FileHash=file_hash),
270
+ timeout=INFO_TIMEOUT,
254
271
  )
255
272
  s = response.Sample
256
273
  return SpliceSample(
@@ -282,7 +299,8 @@ class SpliceGRPCClient:
282
299
  pb2 = self._pb2
283
300
  try:
284
301
  response = await self.stub.ValidateLogin(
285
- pb2.ValidateLoginRequest()
302
+ pb2.ValidateLoginRequest(),
303
+ timeout=CREDITS_TIMEOUT,
286
304
  )
287
305
  return SpliceCredits(
288
306
  credits=response.User.Credits,
@@ -315,7 +333,10 @@ class SpliceGRPCClient:
315
333
  return False
316
334
  pb2 = self._pb2
317
335
  try:
318
- await self.stub.SyncSounds(pb2.SyncSoundsRequest())
336
+ await self.stub.SyncSounds(
337
+ pb2.SyncSoundsRequest(),
338
+ timeout=SYNC_TIMEOUT,
339
+ )
319
340
  return True
320
341
  except Exception as exc:
321
342
  logger.debug("sync_sounds failed: %s", exc)
@@ -6,13 +6,20 @@ These tools are optional — all core tools work without the device.
6
6
 
7
7
  from __future__ import annotations
8
8
 
9
+ import logging
9
10
  import os
11
+ import re # used below in filename parsing helpers
10
12
  from typing import Optional
11
13
 
12
14
  from fastmcp import Context
13
15
 
14
16
  from ..server import mcp, _identify_port_holder
15
17
 
18
+ # Logger must be defined before any helper that uses it — _require_analyzer
19
+ # below calls logger.debug on an exception path, so defining the logger later
20
+ # in the file risked NameError under unusual import orderings.
21
+ logger = logging.getLogger(__name__)
22
+
16
23
  CAPTURE_DIR = os.path.expanduser("~/Documents/LivePilot/captures")
17
24
 
18
25
 
@@ -277,12 +284,6 @@ async def get_clip_file_path(
277
284
  bridge = _get_m4l(ctx)
278
285
  return await bridge.send_command("get_clip_file_path", track_index, clip_index)
279
286
 
280
- import os # for filename parsing in smart-defaults helper
281
- import re
282
- import logging
283
-
284
- logger = logging.getLogger(__name__)
285
-
286
287
  # ── Sample loading helpers (P0-1, P1-1, P2-6 fixes) ────────────────────────
287
288
  #
288
289
  # Critical bug 2026-04-14 (see docs/2026-04-14-bugs-discovered.md):
@@ -114,7 +114,7 @@ def stop_clip(ctx: Context, track_index: int, clip_index: int) -> dict:
114
114
 
115
115
  @mcp.tool()
116
116
  def set_clip_name(ctx: Context, track_index: int, clip_index: int, name: str) -> dict:
117
- """Rename a clip."""
117
+ """Rename a clip in the Session view. The new name appears on the clip slot and in Device Chain displays."""
118
118
  _validate_track_index(track_index)
119
119
  _validate_clip_index(clip_index)
120
120
  if not name.strip():
@@ -144,6 +144,16 @@ def export_clip_midi(
144
144
  else:
145
145
  out_path = _safe_output_path(_output_dir(), filename)
146
146
 
147
+ # Extension guard: after path resolution, confirm the final file really
148
+ # has a MIDI extension. Blocks a model-supplied path like
149
+ # "/etc/cron.d/evil" that accidentally drops its extension through the
150
+ # resolve() step, or a caller that passed "evil.mid/../evil".
151
+ if out_path.suffix.lower() not in {".mid", ".midi"}:
152
+ raise ValueError(
153
+ f"Refusing to write non-MIDI file: {out_path}. "
154
+ f"export_clip_midi requires a .mid or .midi extension."
155
+ )
156
+
147
157
  midi = MIDIFile(1)
148
158
  midi.addTempo(0, 0, tempo)
149
159
 
@@ -106,7 +106,7 @@ def duplicate_track(ctx: Context, track_index: int) -> dict:
106
106
 
107
107
  @mcp.tool()
108
108
  def set_track_name(ctx: Context, track_index: int, name: str) -> dict:
109
- """Rename a track."""
109
+ """Rename a track. The new name appears in both the Session and Arrangement views and survives session save."""
110
110
  _validate_track_index(track_index)
111
111
  if not name.strip():
112
112
  raise ValueError("Track name cannot be empty")
@@ -62,7 +62,7 @@ def start_playback(ctx: Context) -> dict:
62
62
 
63
63
  @mcp.tool()
64
64
  def stop_playback(ctx: Context) -> dict:
65
- """Stop playback."""
65
+ """Stop playback — halts the session transport and the arrangement cursor returns to its last position."""
66
66
  return _get_ableton(ctx).send_command("stop_playback")
67
67
 
68
68
 
@@ -6,11 +6,19 @@ then delegates to pure-computation critics.
6
6
 
7
7
  from __future__ import annotations
8
8
 
9
+ import logging
10
+
9
11
  from fastmcp import Context
10
12
 
11
13
  from ..server import mcp
12
14
  from .critics import build_translation_report, run_all_translation_critics
13
15
 
16
+ # Logger defined at module top: _fetch_translation_data below calls
17
+ # logger.debug on an exception path. The previous version buried the logger
18
+ # definition inside a docstring mid-file, which meant the name was never
19
+ # actually bound at module level — any exception path would raise NameError.
20
+ logger = logging.getLogger(__name__)
21
+
14
22
 
15
23
  # ── Helpers ─────────────────────────────────────────────────────────
16
24
 
@@ -97,10 +105,6 @@ def get_translation_issues(ctx: Context) -> dict:
97
105
 
98
106
  Lighter than check_translation — returns only detected issues
99
107
  from the 5 playback robustness critics.
100
- import logging
101
-
102
- logger = logging.getLogger(__name__)
103
-
104
108
  """
105
109
  mix_snapshot = _fetch_translation_data(ctx)
106
110
  issues = run_all_translation_critics(mix_snapshot)
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "livepilot",
3
- "version": "1.10.7",
3
+ "version": "1.10.8",
4
4
  "mcpName": "io.github.dreamrec/livepilot",
5
- "description": "Agentic production system for Ableton Live 12 — 323 tools, 45 domains. Device atlas (1305 devices), sample engine (Splice + browser + filesystem), auto-composition, spectral perception, technique memory, creative intelligence (12 engines)",
5
+ "description": "Agentic production system for Ableton Live 12 — 324 tools, 45 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",
7
7
  "license": "BSL-1.1",
8
8
  "type": "commonjs",
@@ -43,5 +43,27 @@
43
43
  ],
44
44
  "engines": {
45
45
  "node": ">=18.0.0"
46
- }
46
+ },
47
+ "files": [
48
+ "bin/**/*.js",
49
+ "installer/**/*.js",
50
+ "mcp_server/**/*.py",
51
+ "mcp_server/**/*.json",
52
+ "mcp_server/**/*.yaml",
53
+ "mcp_server/**/*.md",
54
+ "mcp_server/**/*.db",
55
+ "remote_script/**/*.py",
56
+ "m4l_device/LivePilot_Analyzer.amxd",
57
+ "m4l_device/LivePilot_Analyzer.adv",
58
+ "m4l_device/livepilot_bridge.js",
59
+ "m4l_device/BUILD_GUIDE.md",
60
+ "requirements.txt",
61
+ "README.md",
62
+ "LICENSE",
63
+ "CHANGELOG.md",
64
+ "server.json",
65
+ "!**/__pycache__/**",
66
+ "!**/*.pyc",
67
+ "!**/.DS_Store"
68
+ ]
47
69
  }
@@ -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.10.7"
8
+ __version__ = "1.10.8"
9
9
 
10
10
  from _Framework.ControlSurface import ControlSurface
11
11
  from . import router
@@ -46,26 +46,46 @@ _HANDLER_MODULES = (
46
46
  )
47
47
 
48
48
 
49
- def _force_reload_handlers():
49
+ def _force_reload_handlers(cs=None):
50
50
  """Force Python to re-read the handler modules from disk.
51
51
 
52
52
  Called on every create_instance() except the first, so edits to
53
53
  handler files take effect via Control Surface toggle without
54
54
  restarting Ableton. Order matters: router first (clears _handlers),
55
55
  then each handler module (re-registers its @register decorators).
56
+
57
+ When ``cs`` is provided, reload exceptions are logged through the
58
+ ControlSurface so a SyntaxError / NameError in an edited handler is
59
+ surfaced in Live's status log instead of silently swallowed. The
60
+ previous ``except Exception: pass`` turned any bad handler into a
61
+ silent NOT_FOUND at dispatch time with no hint that reload had failed.
56
62
  """
57
63
  import importlib
64
+ def _log(msg):
65
+ if cs is None:
66
+ return
67
+ try:
68
+ cs.log_message("[LivePilot] " + msg)
69
+ except Exception:
70
+ pass
71
+
58
72
  try:
59
73
  importlib.reload(router)
60
- except Exception:
61
- pass
74
+ except Exception as exc:
75
+ _log("reload(router) FAILED — %s: %s. Handlers will be "
76
+ "stale until Ableton restart." % (type(exc).__name__, exc))
62
77
  for mod in _HANDLER_MODULES:
63
78
  try:
64
79
  importlib.reload(mod)
65
- except Exception:
66
- # Don't block Ableton startup on a single bad reload
67
- # the stale version will still work for that handler
68
- pass
80
+ except Exception as exc:
81
+ # Don't block Ableton startup on a single bad reload, but do
82
+ # tell the user what happened the stale handler will keep
83
+ # serving the OLD code until a full restart.
84
+ _log("reload(%s) FAILED — %s: %s. Handler is stale." % (
85
+ getattr(mod, "__name__", "?"),
86
+ type(exc).__name__,
87
+ exc,
88
+ ))
69
89
 
70
90
 
71
91
  def create_instance(c_instance):
@@ -78,7 +98,7 @@ def create_instance(c_instance):
78
98
  """
79
99
  global _FIRST_CREATE_INSTANCE
80
100
  if not _FIRST_CREATE_INSTANCE:
81
- _force_reload_handlers()
101
+ _force_reload_handlers(cs=c_instance)
82
102
  _FIRST_CREATE_INSTANCE = False
83
103
  return LivePilot(c_instance)
84
104
 
@@ -407,11 +407,21 @@ def modify_arrangement_notes(song, params):
407
407
  for note in all_notes:
408
408
  note_map[note.note_id] = note
409
409
 
410
+ # Two-pass: validate all note_ids BEFORE mutating any notes. See the
411
+ # identical fix in notes.py:modify_notes — partial mid-loop mutation on
412
+ # the C++ NoteVector was leaving the clip in a half-modified state that
413
+ # never got committed.
414
+ missing = [int(mod["note_id"]) for mod in modifications
415
+ if int(mod["note_id"]) not in note_map]
416
+ if missing:
417
+ raise ValueError(
418
+ "Note IDs not found in arrangement clip: %s. "
419
+ "No modifications applied." % missing
420
+ )
421
+
410
422
  modified_count = 0
411
423
  for mod in modifications:
412
424
  note_id = int(mod["note_id"])
413
- if note_id not in note_map:
414
- raise ValueError("Note ID %d not found in clip" % note_id)
415
425
  note = note_map[note_id]
416
426
  if "pitch" in mod:
417
427
  note.pitch = int(mod["pitch"])