livepilot 1.26.2 → 1.27.1

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 (69) hide show
  1. package/CHANGELOG.md +92 -0
  2. package/README.md +34 -14
  3. package/bin/livepilot.js +4 -2
  4. package/installer/install.js +21 -12
  5. package/livepilot/.Codex-plugin/plugin.json +2 -2
  6. package/livepilot/.claude-plugin/plugin.json +2 -2
  7. package/livepilot/skills/livepilot-core/SKILL.md +12 -12
  8. package/livepilot/skills/livepilot-core/references/device-knowledge/effects-space.md +2 -1
  9. package/livepilot/skills/livepilot-core/references/overview.md +6 -4
  10. package/livepilot/skills/livepilot-core/references/sound-design.md +5 -2
  11. package/livepilot/skills/livepilot-creative-director/SKILL.md +36 -0
  12. package/livepilot/skills/livepilot-creative-director/references/move-family-diversity-rule.md +1 -1
  13. package/livepilot/skills/livepilot-evaluation/references/capability-modes.md +15 -9
  14. package/livepilot/skills/livepilot-release/SKILL.md +7 -5
  15. package/m4l_device/LivePilot_Analyzer.amxd +0 -0
  16. package/m4l_device/livepilot_bridge.js +1 -1
  17. package/mcp_server/__init__.py +1 -1
  18. package/mcp_server/atlas/__init__.py +20 -1
  19. package/mcp_server/atlas/tools.py +83 -22
  20. package/mcp_server/composer/full/apply.py +9 -0
  21. package/mcp_server/composer/full/layer_planner.py +16 -6
  22. package/mcp_server/creative_constraints/engine.py +46 -0
  23. package/mcp_server/experiment/engine.py +10 -3
  24. package/mcp_server/grader/client.py +30 -2
  25. package/mcp_server/grader/tools.py +8 -2
  26. package/mcp_server/hook_hunter/analyzer.py +15 -2
  27. package/mcp_server/m4l_bridge.py +19 -21
  28. package/mcp_server/memory/taste_graph.py +16 -0
  29. package/mcp_server/memory/technique_store.py +23 -3
  30. package/mcp_server/mix_engine/state_builder.py +4 -2
  31. package/mcp_server/musical_intelligence/detectors.py +11 -2
  32. package/mcp_server/musical_intelligence/tools.py +15 -2
  33. package/mcp_server/preview_studio/engine.py +30 -1
  34. package/mcp_server/project_brain/tools.py +56 -52
  35. package/mcp_server/reference_engine/tools.py +22 -2
  36. package/mcp_server/runtime/capability_probe.py +1 -1
  37. package/mcp_server/runtime/capability_state.py +66 -9
  38. package/mcp_server/runtime/live_version.py +45 -8
  39. package/mcp_server/runtime/safety_kernel.py +11 -0
  40. package/mcp_server/runtime/session_kernel.py +7 -0
  41. package/mcp_server/runtime/tools.py +324 -22
  42. package/mcp_server/sample_engine/critics.py +7 -3
  43. package/mcp_server/sample_engine/slice_workflow.py +3 -2
  44. package/mcp_server/sample_engine/tools.py +53 -21
  45. package/mcp_server/song_brain/builder.py +17 -1
  46. package/mcp_server/sound_design/tools.py +1 -1
  47. package/mcp_server/splice_client/http_bridge.py +43 -1
  48. package/mcp_server/splice_client/models.py +7 -3
  49. package/mcp_server/synthesis_brain/adapters/analog.py +13 -1
  50. package/mcp_server/synthesis_brain/adapters/operator.py +5 -2
  51. package/mcp_server/synthesis_brain/adapters/wavetable.py +4 -4
  52. package/mcp_server/tools/_composition_engine/models.py +6 -0
  53. package/mcp_server/tools/_composition_engine/sections.py +4 -0
  54. package/mcp_server/tools/analyzer.py +38 -1
  55. package/mcp_server/tools/clips.py +5 -4
  56. package/mcp_server/tools/composition.py +5 -1
  57. package/mcp_server/tools/midi_io.py +40 -1
  58. package/mcp_server/tools/transport.py +1 -1
  59. package/mcp_server/user_corpus/plugin_engine/detector.py +66 -11
  60. package/mcp_server/user_corpus/runner.py +7 -1
  61. package/mcp_server/user_corpus/scanners/amxd.py +24 -13
  62. package/mcp_server/user_corpus/tools.py +45 -9
  63. package/mcp_server/wonder_mode/tools.py +66 -27
  64. package/package.json +2 -2
  65. package/remote_script/LivePilot/__init__.py +1 -1
  66. package/remote_script/LivePilot/server.py +23 -3
  67. package/remote_script/LivePilot/version_detect.py +3 -0
  68. package/requirements.txt +4 -4
  69. package/server.json +3 -3
@@ -50,6 +50,20 @@ from .plugin_engine.research import (
50
50
  )
51
51
 
52
52
 
53
+ # ─── Inline plugin serialization helper ────────────────────────────────────────
54
+ # corpus_detect_plugins persists the FULL plugin dict (incl. raw sdk_metadata =
55
+ # entire moduleinfo.json / Info.plist) to _inventory.json. The inline MCP
56
+ # response, however, only needs lightweight identity fields — returning the full
57
+ # sdk_metadata for every plugin bloats the payload and duplicates the file.
58
+ # Callers that need the raw metadata read it back from _inventory.json.
59
+
60
+
61
+ def _slim_plugin(plugin_dict: dict) -> dict:
62
+ """Return a copy of a serialized DetectedPlugin without the heavy
63
+ ``sdk_metadata`` blob, for inline (non-persisted) MCP responses."""
64
+ return {k: v for k, v in plugin_dict.items() if k != "sdk_metadata"}
65
+
66
+
53
67
  # ─── Vendor canonicalization helpers (used by corpus_canonicalize_plugins) ───
54
68
  # Strips common vendor-suffix words ("DSP", "LLC", "GmbH", etc.) so different
55
69
  # spellings of the same vendor ("Valhalla DSP, LLC" + "Valhalladsp") collapse
@@ -390,7 +404,7 @@ def corpus_detect_plugins(
390
404
  )
391
405
 
392
406
  return {
393
- "plugins": plugins_serialized,
407
+ "plugins": [_slim_plugin(p) for p in plugins_serialized],
394
408
  "totals": {"all": len(detected), "by_format": by_format},
395
409
  "inventory_path": str(inventory_path) if inventory_path else None,
396
410
  "search_paths": [str(p) for p, _ in default_plugin_dir()],
@@ -821,6 +835,7 @@ def corpus_research_targets(
821
835
  def corpus_emit_synthesis_briefs(
822
836
  ctx: Context,
823
837
  plugin_ids: list = None,
838
+ inline_limit: int = 5,
824
839
  ) -> dict:
825
840
  """Phase 4 — emit sonnet-subagent briefs for plugin identity synthesis.
826
841
 
@@ -832,10 +847,18 @@ def corpus_emit_synthesis_briefs(
832
847
  ----------
833
848
  plugin_ids : list of plugin_ids to emit briefs for. If empty, emits for
834
849
  every plugin in the inventory.
850
+ inline_limit : maximum number of FULL briefs returned inline (default 5,
851
+ matching the 'Cap parallel subagents at ~5' instruction).
852
+ Any plugins beyond this cap are returned as lightweight stubs
853
+ ({plugin_id, output_path}) in `deferred` so a single call can
854
+ never return a multi-MB response over a large inventory. Pass
855
+ explicit `plugin_ids` (or a larger inline_limit) to get the
856
+ full brief for a specific batch.
835
857
 
836
858
  Returns
837
859
  -------
838
- {briefs: [{plugin_id, brief, output_path}, ...], total}
860
+ {briefs: [{plugin_id, brief, output_path}, ...], deferred: [{plugin_id,
861
+ output_path}, ...], total, inline_count, inline_limit}
839
862
  """
840
863
  inventory_path = DEFAULT_OUTPUT_ROOT / "plugins" / "_inventory.json"
841
864
  if not inventory_path.exists():
@@ -844,7 +867,9 @@ def corpus_emit_synthesis_briefs(
844
867
  all_plugins = inventory.get("plugins", [])
845
868
 
846
869
  target_ids = set(plugin_ids) if plugin_ids else None
870
+ cap = inline_limit if inline_limit and inline_limit > 0 else 5
847
871
  briefs = []
872
+ deferred = []
848
873
  for plugin_dict in all_plugins:
849
874
  pid = plugin_dict.get("plugin_id")
850
875
  if target_ids is not None and pid not in target_ids:
@@ -866,21 +891,32 @@ def corpus_emit_synthesis_briefs(
866
891
  research_root = DEFAULT_OUTPUT_ROOT / "plugins" / pid / "research"
867
892
 
868
893
  brief = build_synthesis_brief(plugin, local_manual, research_root if research_root.exists() else None)
869
- briefs.append({
870
- "plugin_id": pid,
871
- "brief": brief,
872
- "output_path": brief["output_path"],
873
- })
894
+ if len(briefs) < cap:
895
+ briefs.append({
896
+ "plugin_id": pid,
897
+ "brief": brief,
898
+ "output_path": brief["output_path"],
899
+ })
900
+ else:
901
+ deferred.append({
902
+ "plugin_id": pid,
903
+ "output_path": brief["output_path"],
904
+ })
874
905
 
875
906
  return {
876
907
  "briefs": briefs,
877
- "total": len(briefs),
908
+ "deferred": deferred,
909
+ "total": len(briefs) + len(deferred),
910
+ "inline_count": len(briefs),
911
+ "inline_limit": cap,
878
912
  "instruction": (
879
913
  "For each brief: dispatch one sonnet subagent (Agent tool with "
880
914
  "subagent_type='general-purpose', model='sonnet') passing the brief "
881
915
  "as context. The subagent reads brief['synthesis_inputs'] and writes "
882
916
  "the YAML at brief['output_path']. Cap parallel subagents at ~5 to "
883
- "avoid main-context bloat."
917
+ "avoid main-context bloat. Plugins beyond inline_limit are listed in "
918
+ "'deferred' as stubs — re-call corpus_emit_synthesis_briefs with their "
919
+ "plugin_ids to get the full briefs for the next batch."
884
920
  ),
885
921
  }
886
922
 
@@ -17,13 +17,20 @@ from . import engine
17
17
  logger = logging.getLogger(__name__)
18
18
 
19
19
 
20
- def _build_synth_profiles_for_wonder(ctx, request_text: str, diagnosis: dict) -> list:
20
+ def _build_synth_profiles_for_wonder(
21
+ ctx, request_text: str, diagnosis: dict, session_info: dict | None = None
22
+ ) -> list:
21
23
  """Build SynthProfile objects for every track holding a native synth.
22
24
 
23
25
  Wires the synthesis_brain producer into enter_wonder_mode's runtime
24
26
  flow — without this helper, ``propose_synth_branches`` is registered
25
27
  but unreachable from the MCP surface.
26
28
 
29
+ ``session_info``: optional pre-fetched get_session_info payload. When
30
+ a non-empty dict is supplied the helper reuses it instead of issuing
31
+ its own round-trip on the single-client TCP socket. Callers should
32
+ always thread the shared payload to avoid redundant fetches.
33
+
27
34
  Returns a list of SynthProfile objects (possibly empty). All errors
28
35
  are swallowed and logged — missing devices, disconnected Ableton,
29
36
  unsupported devices all land as "no synth branches" rather than
@@ -39,11 +46,16 @@ def _build_synth_profiles_for_wonder(ctx, request_text: str, diagnosis: dict) ->
39
46
  if ableton is None:
40
47
  return []
41
48
 
42
- try:
43
- session = ableton.send_command("get_session_info", {})
44
- except Exception as exc:
45
- logger.debug("session fetch for synth profiles failed: %s", exc)
46
- return []
49
+ # Reuse the pre-fetched session payload when available; only fall back
50
+ # to a fresh round-trip when called without one.
51
+ if isinstance(session_info, dict) and session_info and "error" not in session_info:
52
+ session = session_info
53
+ else:
54
+ try:
55
+ session = ableton.send_command("get_session_info", {})
56
+ except Exception as exc:
57
+ logger.debug("session fetch for synth profiles failed: %s", exc)
58
+ return []
47
59
  if not isinstance(session, dict) or "error" in session:
48
60
  return []
49
61
 
@@ -164,21 +176,35 @@ def _get_ledger_entries(ctx: Context) -> list[dict]:
164
176
  return []
165
177
 
166
178
 
167
- def _get_stuckness_report(ctx: Context, song_brain: dict) -> dict | None:
168
- """Run stuckness detection on recent actions if available."""
179
+ def _get_stuckness_report(
180
+ ctx: Context,
181
+ song_brain: dict,
182
+ action_ledger: list[dict] | None = None,
183
+ session_info: dict | None = None,
184
+ ) -> dict | None:
185
+ """Run stuckness detection on recent actions if available.
186
+
187
+ ``action_ledger`` and ``session_info`` may be supplied pre-fetched by
188
+ the caller. enter_wonder_mode already fetches both once at the top of
189
+ the flow, so threading them here avoids a duplicate ledger read and a
190
+ redundant get_session_info round-trip on the single-client TCP socket.
191
+ """
169
192
  try:
170
193
  from ..stuckness_detector.detector import detect_stuckness
171
- action_ledger = _get_ledger_entries(ctx)
194
+ if action_ledger is None:
195
+ action_ledger = _get_ledger_entries(ctx)
172
196
  if not action_ledger:
173
197
  return None
174
- # Pass session_info if available for better accuracy
175
- session_info = {}
176
- try:
177
- ableton = ctx.lifespan_context.get("ableton")
178
- if ableton:
179
- session_info = ableton.send_command("get_session_info", {})
180
- except Exception as exc:
181
- logger.warning("session_info fetch for stuckness failed: %s", exc)
198
+ # Reuse the pre-fetched session payload; only round-trip if the
199
+ # caller did not supply one.
200
+ if session_info is None:
201
+ session_info = {}
202
+ try:
203
+ ableton = ctx.lifespan_context.get("ableton")
204
+ if ableton:
205
+ session_info = ableton.send_command("get_session_info", {})
206
+ except Exception as exc:
207
+ logger.warning("session_info fetch for stuckness failed: %s", exc)
182
208
  report = detect_stuckness(
183
209
  action_history=action_ledger,
184
210
  session_info=session_info,
@@ -218,7 +244,24 @@ def enter_wonder_mode(
218
244
  taste_graph = _get_taste_graph(ctx)
219
245
  active_constraints = _get_active_constraints()
220
246
  action_ledger = _get_ledger_entries(ctx)
221
- stuckness_report = _get_stuckness_report(ctx, song_brain)
247
+
248
+ # Single get_session_info round-trip for the whole flow. The Remote
249
+ # Script is single-client on TCP 9878, so each redundant fetch is a
250
+ # serialized round-trip — fetch once and thread the payload into the
251
+ # stuckness report, the kernel dict, and the synth-profile builder.
252
+ session_info: dict = {}
253
+ try:
254
+ ableton = ctx.lifespan_context.get("ableton")
255
+ if ableton:
256
+ session_info = ableton.send_command("get_session_info", {})
257
+ except Exception as exc:
258
+ logger.warning("session_info fetch failed: %s", exc)
259
+ if not isinstance(session_info, dict) or "error" in session_info:
260
+ session_info = {}
261
+
262
+ stuckness_report = _get_stuckness_report(
263
+ ctx, song_brain, action_ledger=action_ledger, session_info=session_info
264
+ )
222
265
 
223
266
  # 1. Build diagnosis
224
267
  diagnosis = build_diagnosis(
@@ -249,14 +292,8 @@ def enter_wonder_mode(
249
292
  # Graceful degradation — analytical variants still work
250
293
  logger.warning("sample opportunity search failed: %s", exc)
251
294
 
252
- # 1c. Get session info for kernel
253
- session_info = {}
254
- try:
255
- ableton = ctx.lifespan_context.get("ableton")
256
- if ableton:
257
- session_info = ableton.send_command("get_session_info", {})
258
- except Exception as exc:
259
- logger.warning("session_info fetch for kernel failed: %s", exc)
295
+ # 1c. session_info for the kernel was already fetched once above and
296
+ # threaded into the stuckness report — reuse it here, no extra round-trip.
260
297
 
261
298
  # 2. Generate variants (legacy path)
262
299
  result = engine.generate_wonder_variants(
@@ -291,7 +328,9 @@ def enter_wonder_mode(
291
328
  # track that holds a native synth. Lets propose_synth_branches reach
292
329
  # the runtime — without this the producer is dark despite being
293
330
  # registered in the conductor.
294
- synth_profiles = _build_synth_profiles_for_wonder(ctx, request_text, diag_dict)
331
+ synth_profiles = _build_synth_profiles_for_wonder(
332
+ ctx, request_text, diag_dict, session_info=session_info
333
+ )
295
334
 
296
335
  # Composer branches fire when the base conductor routes to
297
336
  # composition — otherwise we'd be emitting composition scaffolding
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "livepilot",
3
- "version": "1.26.2",
3
+ "version": "1.27.1",
4
4
  "mcpName": "io.github.dreamrec/livepilot",
5
- "description": "Agentic production system for Ableton Live 12 — 465 tools, 56 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 — 467 tools, 56 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.26.2"
8
+ __version__ = "1.27.1"
9
9
 
10
10
  from _Framework.ControlSurface import ControlSurface
11
11
  from . import router
@@ -346,9 +346,14 @@ class LivePilotServer(object):
346
346
  else:
347
347
  timeout = 10
348
348
 
349
- # Per-command response queue
349
+ # Per-command response queue + cancellation flag. The flag is shared
350
+ # between this TCP thread and the main thread that dequeues the item:
351
+ # if we time out below, we set it so _process_next_command skips the
352
+ # (now-abandoned) dispatch instead of mutating Live after the client
353
+ # has already been told the command timed out (phantom write).
350
354
  response_queue = queue.Queue()
351
- self._command_queue.put((command, response_queue))
355
+ cancelled = threading.Event()
356
+ self._command_queue.put((command, response_queue, cancelled))
352
357
 
353
358
  # Schedule processing on Ableton's main thread
354
359
  try:
@@ -385,6 +390,13 @@ class LivePilotServer(object):
385
390
  try:
386
391
  resp = response_queue.get(timeout=timeout)
387
392
  except queue.Empty:
393
+ # Mark the queued command as abandoned. If it hasn't been dequeued
394
+ # yet (or is awaiting its settle hop), _process_next_command sees
395
+ # the flag and skips router.dispatch — preventing a write that the
396
+ # client was just told timed out from executing on Live's main
397
+ # thread later. If dispatch already happened, setting the flag is
398
+ # a harmless no-op.
399
+ cancelled.set()
388
400
  resp = {
389
401
  "id": request_id,
390
402
  "ok": False,
@@ -399,10 +411,18 @@ class LivePilotServer(object):
399
411
  """Called on Ableton's main thread via schedule_message.
400
412
  Processes one command from the queue."""
401
413
  try:
402
- command, response_queue = self._command_queue.get_nowait()
414
+ command, response_queue, cancelled = self._command_queue.get_nowait()
403
415
  except queue.Empty:
404
416
  return
405
417
 
418
+ # The TCP thread already gave up on this command (timed out) and told
419
+ # the client so. Do NOT dispatch it — running the write now would be a
420
+ # phantom mutation. Nobody is waiting on response_queue, so just keep
421
+ # the main-thread pump alive by draining whatever is left.
422
+ if cancelled.is_set():
423
+ self._drain_queue()
424
+ return
425
+
406
426
  cmd_type = command.get("type", "")
407
427
  is_write = is_write_command(cmd_type)
408
428
 
@@ -25,6 +25,9 @@ FEATURES = {
25
25
  "drum_chain_in_note": (12, 3, 0),
26
26
  "stem_separation": (12, 3, 0),
27
27
  "device_ab_compare": (12, 3, 0),
28
+ "link_audio": (12, 4, 0),
29
+ "stem_time_selection": (12, 4, 0),
30
+ "stem_merge_selected": (12, 4, 0),
28
31
  "replace_sample_native": (12, 4, 0),
29
32
  "groove_pool_api": (11, 0, 0),
30
33
  "rack_variations_api": (11, 0, 0),
package/requirements.txt CHANGED
@@ -1,19 +1,19 @@
1
1
  # LivePilot MCP Server dependencies
2
2
  numpy>=2.4.6
3
- fastmcp>=3.3.1,<3.4.0 # pinned upper bound — _get_all_tools() accesses private internals
3
+ fastmcp>=3.4.2,<3.5.0 # pinned upper bound — _get_all_tools() accesses private internals
4
4
  midiutil>=1.2.1
5
5
  pretty_midi>=0.2.11
6
6
  # v1.8 Perception Layer (offline analysis)
7
7
  pyloudnorm>=0.2.0
8
- soundfile>=0.13.1
8
+ soundfile>=0.14.0
9
9
  scipy>=1.17.1
10
10
  mutagen>=1.47.0
11
11
  # v1.10.5 Splice online catalog integration — required, not optional.
12
12
  # Without these, SpliceGRPCClient silently disables itself and search_samples
13
13
  # falls back to the SQLite sounds.db which only returns locally downloaded
14
14
  # samples (see docs/2026-04-14-bugs-discovered.md — P0-2).
15
- grpcio>=1.80.0
16
- protobuf>=7.35.0
15
+ grpcio>=1.81.1
16
+ protobuf>=7.35.1
17
17
 
18
18
  # Known benign warning during install:
19
19
  # ERROR: pip's dependency resolver does not currently take into account
package/server.json CHANGED
@@ -1,17 +1,17 @@
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": "465-tool agentic MCP production system for Ableton Live 12 \u2014 56 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": "467-tool agentic MCP production system for Ableton Live 12 \u2014 56 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.26.2",
9
+ "version": "1.27.1",
10
10
  "packages": [
11
11
  {
12
12
  "registryType": "npm",
13
13
  "identifier": "livepilot",
14
- "version": "1.26.2",
14
+ "version": "1.27.1",
15
15
  "transport": {
16
16
  "type": "stdio"
17
17
  }