livepilot 1.10.4 → 1.10.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.
Files changed (74) hide show
  1. package/.claude-plugin/marketplace.json +3 -3
  2. package/AGENTS.md +3 -3
  3. package/CHANGELOG.md +148 -0
  4. package/CONTRIBUTING.md +1 -1
  5. package/README.md +6 -6
  6. package/livepilot/.Codex-plugin/plugin.json +2 -2
  7. package/livepilot/.claude-plugin/plugin.json +2 -2
  8. package/livepilot/skills/livepilot-core/SKILL.md +4 -4
  9. package/livepilot/skills/livepilot-core/references/overview.md +3 -3
  10. package/livepilot/skills/livepilot-evaluation/references/capability-modes.md +1 -1
  11. package/livepilot/skills/livepilot-release/SKILL.md +5 -5
  12. package/m4l_device/LivePilot_Analyzer.amxd +0 -0
  13. package/m4l_device/livepilot_bridge.js +12 -1
  14. package/manifest.json +3 -3
  15. package/mcp_server/__init__.py +1 -1
  16. package/mcp_server/composer/sample_resolver.py +10 -6
  17. package/mcp_server/composer/tools.py +10 -6
  18. package/mcp_server/connection.py +6 -1
  19. package/mcp_server/creative_constraints/tools.py +9 -8
  20. package/mcp_server/experiment/engine.py +9 -5
  21. package/mcp_server/experiment/tools.py +9 -9
  22. package/mcp_server/hook_hunter/tools.py +14 -9
  23. package/mcp_server/m4l_bridge.py +11 -0
  24. package/mcp_server/memory/taste_graph.py +7 -2
  25. package/mcp_server/mix_engine/tools.py +8 -3
  26. package/mcp_server/musical_intelligence/tools.py +15 -10
  27. package/mcp_server/performance_engine/tools.py +6 -2
  28. package/mcp_server/preview_studio/tools.py +21 -15
  29. package/mcp_server/project_brain/tools.py +18 -10
  30. package/mcp_server/reference_engine/tools.py +7 -5
  31. package/mcp_server/runtime/capability_probe.py +10 -4
  32. package/mcp_server/runtime/tools.py +8 -2
  33. package/mcp_server/sample_engine/tools.py +394 -33
  34. package/mcp_server/semantic_moves/tools.py +5 -1
  35. package/mcp_server/server.py +10 -9
  36. package/mcp_server/services/motif_service.py +9 -3
  37. package/mcp_server/session_continuity/tools.py +7 -3
  38. package/mcp_server/session_continuity/tracker.py +9 -8
  39. package/mcp_server/song_brain/tools.py +17 -12
  40. package/mcp_server/splice_client/client.py +19 -6
  41. package/mcp_server/stuckness_detector/tools.py +8 -5
  42. package/mcp_server/tools/_agent_os_engine/__init__.py +52 -0
  43. package/mcp_server/tools/_agent_os_engine/critics.py +134 -0
  44. package/mcp_server/tools/_agent_os_engine/evaluation.py +206 -0
  45. package/mcp_server/tools/_agent_os_engine/models.py +132 -0
  46. package/mcp_server/tools/_agent_os_engine/taste.py +192 -0
  47. package/mcp_server/tools/_agent_os_engine/techniques.py +161 -0
  48. package/mcp_server/tools/_agent_os_engine/world_model.py +170 -0
  49. package/mcp_server/tools/_composition_engine/__init__.py +67 -0
  50. package/mcp_server/tools/_composition_engine/analysis.py +174 -0
  51. package/mcp_server/tools/_composition_engine/critics.py +522 -0
  52. package/mcp_server/tools/_composition_engine/gestures.py +230 -0
  53. package/mcp_server/tools/_composition_engine/harmony.py +70 -0
  54. package/mcp_server/tools/_composition_engine/models.py +193 -0
  55. package/mcp_server/tools/_composition_engine/sections.py +371 -0
  56. package/mcp_server/tools/_perception_engine.py +18 -11
  57. package/mcp_server/tools/agent_os.py +23 -15
  58. package/mcp_server/tools/analyzer.py +166 -7
  59. package/mcp_server/tools/automation.py +6 -1
  60. package/mcp_server/tools/composition.py +25 -16
  61. package/mcp_server/tools/devices.py +10 -6
  62. package/mcp_server/tools/motif.py +7 -2
  63. package/mcp_server/tools/planner.py +6 -2
  64. package/mcp_server/tools/research.py +13 -10
  65. package/mcp_server/transition_engine/tools.py +6 -1
  66. package/mcp_server/translation_engine/tools.py +8 -6
  67. package/mcp_server/wonder_mode/engine.py +8 -3
  68. package/mcp_server/wonder_mode/tools.py +29 -21
  69. package/package.json +2 -2
  70. package/remote_script/LivePilot/__init__.py +1 -1
  71. package/requirements.txt +6 -0
  72. package/livepilot.mcpb +0 -0
  73. package/mcp_server/tools/_agent_os_engine.py +0 -947
  74. package/mcp_server/tools/_composition_engine.py +0 -1530
@@ -43,7 +43,8 @@ def _require_analyzer(cache) -> None:
43
43
  ctx.lifespan_context["ableton"].send_command("get_master_track")
44
44
  if ctx else {}
45
45
  )
46
- except Exception:
46
+ except Exception as exc:
47
+ logger.debug("_require_analyzer failed: %s", exc)
47
48
  track = {}
48
49
 
49
50
  devices = track.get("devices", []) if isinstance(track, dict) else []
@@ -254,7 +255,6 @@ async def walk_device_tree(
254
255
  bridge = _get_m4l(ctx)
255
256
  return await bridge.send_command("walk_rack", track_index, device_index)
256
257
 
257
-
258
258
  # ── Phase 2: Sample Operations ─────────────────────────────────────────
259
259
 
260
260
 
@@ -276,6 +276,130 @@ async def get_clip_file_path(
276
276
  bridge = _get_m4l(ctx)
277
277
  return await bridge.send_command("get_clip_file_path", track_index, clip_index)
278
278
 
279
+ import os # for filename parsing in smart-defaults helper
280
+ import re
281
+ import logging
282
+
283
+ logger = logging.getLogger(__name__)
284
+
285
+ # ── Sample loading helpers (P0-1, P1-1, P2-6 fixes) ────────────────────────
286
+ #
287
+ # Critical bug 2026-04-14 (see docs/2026-04-14-bugs-discovered.md):
288
+ #
289
+ # The M4L bridge's `replace_simpler_sample` command can report success even
290
+ # when the sample is still the bootstrap placeholder. The Simpler's display
291
+ # name also does NOT refresh after a replace. After loading, Simpler's Snap
292
+ # parameter is ON by default which causes the Sample Start position to
293
+ # snap to a location outside the new sample's valid audio — resulting in
294
+ # silent playback.
295
+ #
296
+ # The fixes below:
297
+ # 1. After replace, verify by reading the actual device name via
298
+ # get_track_info and comparing to the expected filename stem. If the
299
+ # name doesn't match, return a clear error so the caller doesn't
300
+ # silently ship the wrong audio.
301
+ # 2. Auto-set Snap=0 to disarm the zero-crossing snap that breaks playback.
302
+ # 3. For WARPED LOOPS (detected by "NNbpm" in the filename), set
303
+ # S Start=0, S Length=1, S Loop On=1 so the full loop plays in its
304
+ # musical phrasing. For ONE-SHOTS, leave defaults alone.
305
+
306
+ _BPM_IN_FILENAME_RE = re.compile(r"(\d{2,3})\s*bpm", re.IGNORECASE)
307
+
308
+
309
+ def _is_warped_loop(file_path: str) -> bool:
310
+ """Return True if the filename contains a BPM marker (likely a tempo-locked loop)."""
311
+ stem = os.path.splitext(os.path.basename(file_path))[0]
312
+ return bool(_BPM_IN_FILENAME_RE.search(stem))
313
+
314
+
315
+ def _filename_stem(file_path: str) -> str:
316
+ return os.path.splitext(os.path.basename(file_path))[0]
317
+
318
+
319
+ async def _simpler_post_load_hygiene(
320
+ bridge,
321
+ ableton,
322
+ track_index: int,
323
+ device_index: int,
324
+ file_path: str,
325
+ ) -> dict:
326
+ """Apply post-load hygiene to a newly loaded Simpler and verify success.
327
+
328
+ Steps:
329
+ 1. Read track info to verify the device's actual name matches the
330
+ expected sample stem. If it doesn't, return an error.
331
+ 2. Set Snap=0 (Off) — required so sample playback works.
332
+ 3. If filename indicates a warped loop, set S Start=0, S Length=1,
333
+ S Loop On=1 so the loop plays fully instead of being cropped.
334
+ 4. Return a verified response dict.
335
+ """
336
+ expected_stem = _filename_stem(file_path)
337
+
338
+ # Step 1: verify device name matches expected file
339
+ try:
340
+ track_info = ableton.send_command(
341
+ "get_track_info", {"track_index": track_index}
342
+ )
343
+ except Exception as exc:
344
+ return {"error": f"Verification read failed: {exc}"}
345
+
346
+ devices = track_info.get("devices", []) or []
347
+ if device_index < 0 or device_index >= len(devices):
348
+ return {
349
+ "error": (
350
+ f"Device index {device_index} out of range after load "
351
+ f"(track has {len(devices)} devices)"
352
+ ),
353
+ "verified": False,
354
+ }
355
+ device = devices[device_index]
356
+ actual_name = str(device.get("name") or "")
357
+ verified = expected_stem in actual_name or actual_name in expected_stem
358
+ if not verified:
359
+ return {
360
+ "error": (
361
+ f"Sample verification FAILED — Simpler name '{actual_name}' "
362
+ f"does not match requested file '{expected_stem}'. The bridge "
363
+ f"reported success but the actual sample is different. "
364
+ f"Try `load_browser_item` with a user_library URI instead."
365
+ ),
366
+ "verified": False,
367
+ "actual_device_name": actual_name,
368
+ "expected_stem": expected_stem,
369
+ }
370
+
371
+ # Step 2: turn Snap OFF — required for reliable playback after replace
372
+ hygiene_params: list[dict] = [
373
+ {"name_or_index": "Snap", "value": 0},
374
+ ]
375
+
376
+ # Step 3: smart defaults for warped loops
377
+ if _is_warped_loop(file_path):
378
+ hygiene_params.extend([
379
+ {"name_or_index": "S Start", "value": 0.0},
380
+ {"name_or_index": "S Length", "value": 1.0},
381
+ {"name_or_index": "S Loop On", "value": 1},
382
+ ])
383
+
384
+ try:
385
+ ableton.send_command("batch_set_parameters", {
386
+ "track_index": track_index,
387
+ "device_index": device_index,
388
+ "parameters": hygiene_params,
389
+ })
390
+ except Exception as exc:
391
+ logger.debug("_simpler_post_load_hygiene failed: %s", exc)
392
+ # non-fatal — verification already succeeded
393
+ pass
394
+
395
+ return {
396
+ "verified": True,
397
+ "device_name": actual_name,
398
+ "track_index": track_index,
399
+ "device_index": device_index,
400
+ "warped_loop_defaults_applied": _is_warped_loop(file_path),
401
+ }
402
+
279
403
 
280
404
  @mcp.tool()
281
405
  async def replace_simpler_sample(
@@ -292,6 +416,17 @@ async def replace_simpler_sample(
292
416
  manually first or use find_and_load_device to load a preset that already
293
417
  contains a sample.
294
418
 
419
+ **Prefer `load_browser_item(track, uri)` when possible** — see P0-1 in
420
+ docs/2026-04-14-bugs-discovered.md. The M4L bridge's replace path can
421
+ silently keep the bootstrap placeholder in some conditions; this tool
422
+ now verifies by reading back the device name and will return an error
423
+ if the replace didn't actually take effect.
424
+
425
+ Also auto-applies post-load hygiene:
426
+ - Sets Simpler Snap=0 (required for playback after replace)
427
+ - For warped loops (filename contains 'NNbpm'), sets S Start=0,
428
+ S Length=1, S Loop On=1
429
+
295
430
  Use get_clip_file_path to get the path of a resampled clip, then pass
296
431
  it here to load it into Simpler for slicing.
297
432
  Requires LivePilot Analyzer on master track.
@@ -299,6 +434,7 @@ async def replace_simpler_sample(
299
434
  cache = _get_spectral(ctx)
300
435
  _require_analyzer(cache)
301
436
  bridge = _get_m4l(ctx)
437
+ ableton = ctx.lifespan_context["ableton"]
302
438
  result = await bridge.send_command(
303
439
  "replace_simpler_sample", track_index, device_index, file_path
304
440
  )
@@ -312,6 +448,16 @@ async def replace_simpler_sample(
312
448
  "error": "Sample may not have loaded. Ensure the Simpler already "
313
449
  "has a sample loaded — replace_sample silently fails on empty Simplers."
314
450
  }
451
+
452
+ # Verify by reading back the device name — guards against the silent
453
+ # failure mode where the bridge reports success but keeps the placeholder.
454
+ hygiene = await _simpler_post_load_hygiene(
455
+ bridge, ableton, track_index, device_index, file_path
456
+ )
457
+ if not hygiene.get("verified"):
458
+ return hygiene
459
+
460
+ result.update(hygiene)
315
461
  return result
316
462
 
317
463
 
@@ -327,10 +473,18 @@ async def load_sample_to_simpler(
327
473
  This is the full workflow for programmatic sample loading:
328
474
  1. Loads a dummy sample via the browser (creates Simpler with a sample)
329
475
  2. Replaces the dummy with your audio file
330
- 3. Returns the Simpler ready for slicing/warping
476
+ 3. Applies post-load hygiene (Snap=0, loop defaults for warped loops)
477
+ 4. Verifies by reading back the device name — returns an error if
478
+ the Simpler still has the bootstrap placeholder (P0-1 guard)
331
479
 
332
480
  Use this instead of replace_simpler_sample when the track has no Simpler
333
481
  or the Simpler is empty. Works with any audio file path.
482
+
483
+ **For files that exist in Ableton's browser index** (Samples, User Library,
484
+ Packs), PREFER `load_browser_item(track, uri)` — it goes through Ableton's
485
+ native loading path and is more reliable. This tool is a workaround for
486
+ files that aren't browser-indexed.
487
+
334
488
  Requires LivePilot Analyzer on master track.
335
489
  """
336
490
  cache = _get_spectral(ctx)
@@ -380,6 +534,14 @@ async def load_sample_to_simpler(
380
534
  if not result.get("sample_loaded"):
381
535
  return {"error": "Sample replacement failed after bootstrap"}
382
536
 
537
+ # Step 4: Verify by reading back the device name (P0-1 guard)
538
+ hygiene = await _simpler_post_load_hygiene(
539
+ bridge, ableton, track_index, actual_device_index, file_path
540
+ )
541
+ if not hygiene.get("verified"):
542
+ return hygiene
543
+
544
+ result.update(hygiene)
383
545
  result["method"] = "bootstrap_and_replace"
384
546
  result["device_index"] = actual_device_index # additive — for step-result binding
385
547
  result["track_index"] = track_index
@@ -457,7 +619,6 @@ async def warp_simpler(
457
619
  bridge = _get_m4l(ctx)
458
620
  return await bridge.send_command("warp_simpler", track_index, device_index, beats)
459
621
 
460
-
461
622
  # ── Phase 2: Warp Markers ──────────────────────────────────────────────
462
623
 
463
624
 
@@ -542,7 +703,6 @@ async def remove_warp_marker(
542
703
  "remove_warp_marker", track_index, clip_index, beat_time
543
704
  )
544
705
 
545
-
546
706
  # ── Phase 2: Clip & Display ────────────────────────────────────────────
547
707
 
548
708
 
@@ -600,7 +760,6 @@ async def get_display_values(
600
760
  bridge = _get_m4l(ctx)
601
761
  return await bridge.send_command("get_display_values", track_index, device_index, timeout=15.0)
602
762
 
603
-
604
763
  # ── Phase 3: Audio Capture ─────────────────────────────────────────────
605
764
 
606
765
 
@@ -659,6 +818,7 @@ async def capture_audio(
659
818
  dst_path = os.path.join(CAPTURE_DIR, dst_name)
660
819
  try:
661
820
  import shutil
821
+
662
822
  shutil.move(src_path, dst_path)
663
823
  result["file_path"] = dst_path
664
824
  except OSError:
@@ -684,7 +844,6 @@ async def capture_stop(ctx: Context) -> dict:
684
844
  await bridge.cancel_capture_future()
685
845
  return await bridge.send_command("capture_stop")
686
846
 
687
-
688
847
  # ── Phase 4: FluCoMa Real-Time ───────────────────────────────────────────
689
848
 
690
849
  PITCH_NAMES = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"]
@@ -14,6 +14,9 @@ from pydantic import BaseModel, Field
14
14
 
15
15
  from ..curves import generate_curve, generate_from_recipe, list_recipes
16
16
  from ..server import mcp
17
+ import logging
18
+
19
+ logger = logging.getLogger(__name__)
17
20
 
18
21
 
19
22
  def _get_ableton(ctx: Context):
@@ -23,6 +26,7 @@ def _get_ableton(ctx: Context):
23
26
  def _ensure_list(v: Any) -> list:
24
27
  if isinstance(v, str):
25
28
  import json
29
+
26
30
  try:
27
31
  return json.loads(v)
28
32
  except json.JSONDecodeError as exc:
@@ -372,7 +376,8 @@ def apply_automation_recipe(
372
376
  if abs(p_max - p_min) > 1.5 or p_min < -0.5:
373
377
  for pt in points:
374
378
  pt["value"] = p_min + pt["value"] * (p_max - p_min)
375
- except Exception:
379
+ except Exception as exc:
380
+ logger.debug("apply_automation_recipe failed: %s", exc)
376
381
  pass # Fail open — write values as-is if we can't read the range
377
382
 
378
383
  # Safety clamp: auto_pan amplitude should be limited to avoid full L/R swing
@@ -14,6 +14,7 @@ These tools power the composition intelligence layer:
14
14
  from __future__ import annotations
15
15
 
16
16
  import json
17
+ import logging
17
18
  from typing import Optional
18
19
 
19
20
  from fastmcp import Context
@@ -22,6 +23,8 @@ from ..server import mcp
22
23
  from ..memory.technique_store import TechniqueStore
23
24
  from . import _composition_engine as engine
24
25
 
26
+ logger = logging.getLogger(__name__)
27
+
25
28
  _memory_store = TechniqueStore()
26
29
 
27
30
 
@@ -49,7 +52,8 @@ def _build_clip_matrix(ableton, scene_count: int, track_count: int) -> list[list
49
52
  matrix_data = ableton.send_command("get_scene_matrix")
50
53
  raw_matrix = matrix_data.get("matrix", [])
51
54
  return raw_matrix
52
- except Exception:
55
+ except Exception as exc:
56
+ logger.warning("get_scene_matrix failed, using empty matrix: %s", exc)
53
57
  return [[] for _ in range(scene_count)]
54
58
 
55
59
 
@@ -95,8 +99,8 @@ def analyze_composition(ctx: Context) -> dict:
95
99
  clips = arr.get("clips", [])
96
100
  if clips:
97
101
  arr_clips[track["index"]] = clips
98
- except Exception:
99
- pass
102
+ except Exception as exc:
103
+ logger.debug("arrangement_clips track=%s skipped: %s", track.get("index"), exc)
100
104
 
101
105
  if not sections and arr_clips:
102
106
  sections = engine.build_section_graph_from_arrangement(
@@ -111,7 +115,8 @@ def analyze_composition(ctx: Context) -> dict:
111
115
  "track_index": track["index"]
112
116
  })
113
117
  track_data.append(ti)
114
- except Exception:
118
+ except Exception as exc:
119
+ logger.debug("get_track_info track=%s fallback: %s", track.get("index"), exc)
115
120
  track_data.append({"index": track["index"], "name": track.get("name", ""),
116
121
  "devices": []})
117
122
 
@@ -130,8 +135,8 @@ def analyze_composition(ctx: Context) -> dict:
130
135
  })
131
136
  notes = result.get("notes", [])
132
137
  track_notes.extend(notes)
133
- except Exception:
134
- pass
138
+ except Exception as exc:
139
+ logger.debug("get_notes t=%d s=%d skipped: %s", t_idx, s_idx, exc)
135
140
  all_notes_by_track[t_idx] = track_notes
136
141
 
137
142
  # Map notes to sections
@@ -244,7 +249,8 @@ def get_phrase_grid(
244
249
  "clip_index": scene_idx,
245
250
  })
246
251
  notes_by_track[t_idx] = result.get("notes", [])
247
- except Exception:
252
+ except Exception as exc:
253
+ logger.debug("get_notes t=%d s=%d empty: %s", t_idx, scene_idx, exc)
248
254
  notes_by_track[t_idx] = []
249
255
 
250
256
  phrases = engine.detect_phrases(section, notes_by_track)
@@ -464,8 +470,8 @@ def get_harmony_field(
464
470
  "pattern": pattern,
465
471
  "classification": classification,
466
472
  }
467
- except Exception:
468
- pass
473
+ except Exception as exc:
474
+ logger.warning("neo-Riemannian classify failed: %s", exc)
469
475
 
470
476
  # Populate voice_leading_info from chord groups
471
477
  if harmony_analysis and not voice_leading_info:
@@ -483,12 +489,13 @@ def get_harmony_field(
483
489
  "issue_count": len(all_vl_issues),
484
490
  "quality": "clean" if not all_vl_issues else "has_issues",
485
491
  }
486
- except Exception:
487
- pass
492
+ except Exception as exc:
493
+ logger.warning("voice_leading analysis failed: %s", exc)
488
494
 
489
495
  if scale_info and harmony_analysis:
490
496
  break
491
- except Exception:
497
+ except Exception as exc:
498
+ logger.debug("harmony scan on track %d skipped: %s", t_idx, exc)
492
499
  continue
493
500
 
494
501
  hf = engine.build_harmony_field(
@@ -535,7 +542,8 @@ def get_transition_analysis(ctx: Context) -> dict:
535
542
  try:
536
543
  ti = ableton.send_command("get_track_info", {"track_index": t_idx})
537
544
  track_data.append(ti)
538
- except Exception:
545
+ except Exception as exc:
546
+ logger.debug("get_track_info transition t=%d fallback: %s", t_idx, exc)
539
547
  track_data.append({"index": t_idx, "name": track.get("name", ""), "devices": []})
540
548
 
541
549
  for section in sections:
@@ -623,7 +631,8 @@ def get_section_outcomes(
623
631
  techniques = _memory_store.list_techniques(
624
632
  type_filter="composition_outcome", sort_by="updated_at", limit=limit,
625
633
  )
626
- except Exception:
634
+ except Exception as exc:
635
+ logger.warning("list_techniques(composition_outcome) failed: %s", exc)
627
636
  techniques = []
628
637
 
629
638
  outcomes = []
@@ -633,8 +642,8 @@ def get_section_outcomes(
633
642
  payload = full.get("payload", {})
634
643
  if isinstance(payload, dict):
635
644
  outcomes.append(payload)
636
- except Exception:
637
- pass
645
+ except Exception as exc:
646
+ logger.debug("technique %s payload read failed: %s", t.get("id"), exc)
638
647
 
639
648
  result = engine.analyze_section_outcomes(outcomes)
640
649
 
@@ -11,6 +11,10 @@ from typing import Any, Optional
11
11
  from fastmcp import Context
12
12
 
13
13
  from ..server import mcp, _identify_port_holder
14
+ import logging
15
+
16
+ logger = logging.getLogger(__name__)
17
+
14
18
 
15
19
 
16
20
  def _ensure_list(value: Any) -> list:
@@ -130,9 +134,9 @@ def _postflight_loaded_device(ctx: Context, result: dict) -> dict:
130
134
  track_info = _get_ableton(ctx).send_command("get_track_info", {
131
135
  "track_index": int(track_index),
132
136
  })
133
- except Exception:
137
+ except Exception as exc:
138
+ logger.debug("_postflight_loaded_device failed: %s", exc)
134
139
  return annotated
135
-
136
140
  devices = track_info.get("devices", []) if isinstance(track_info, dict) else []
137
141
  if not isinstance(devices, list) or not devices:
138
142
  return annotated
@@ -156,9 +160,8 @@ def _postflight_loaded_device(ctx: Context, result: dict) -> dict:
156
160
  "device_index": int(match["index"]),
157
161
  })
158
162
  param_count = full_info.get("parameter_count", 0)
159
- except Exception:
160
- pass
161
-
163
+ except Exception as exc:
164
+ logger.debug("_postflight_loaded_device failed: %s", exc)
162
165
  device_info = _annotate_device_info({
163
166
  "name": match.get("name"),
164
167
  "class_name": match.get("class_name"),
@@ -597,7 +600,8 @@ def _require_analyzer(cache) -> None:
597
600
  ctx.lifespan_context["ableton"].send_command("get_master_track")
598
601
  if ctx else {}
599
602
  )
600
- except Exception:
603
+ except Exception as exc:
604
+ logger.debug("_require_analyzer failed: %s", exc)
601
605
  track = {}
602
606
 
603
607
  devices = track.get("devices", []) if isinstance(track, dict) else []
@@ -11,6 +11,10 @@ from fastmcp import Context
11
11
 
12
12
  from ..server import mcp
13
13
  from . import _motif_engine as motif_engine
14
+ import logging
15
+
16
+ logger = logging.getLogger(__name__)
17
+
14
18
 
15
19
 
16
20
  def _get_ableton(ctx: Context):
@@ -46,8 +50,9 @@ def get_motif_graph(ctx: Context) -> dict:
46
50
  "clip_index": clip_idx,
47
51
  })
48
52
  track_notes.extend(result.get("notes", []))
49
- except Exception:
50
- pass
53
+ except Exception as exc:
54
+ logger.debug("get_motif_graph failed: %s", exc)
55
+
51
56
  if track_notes:
52
57
  notes_by_track[t_idx] = track_notes
53
58
 
@@ -17,6 +17,9 @@ from fastmcp import Context
17
17
  from ..server import mcp
18
18
  from . import _composition_engine as comp_engine
19
19
  from . import _planner_engine as planner_engine
20
+ import logging
21
+
22
+ logger = logging.getLogger(__name__)
20
23
 
21
24
 
22
25
  def _get_ableton(ctx: Context):
@@ -67,7 +70,8 @@ def plan_arrangement(
67
70
  try:
68
71
  ti = ableton.send_command("get_track_info", {"track_index": t_idx})
69
72
  track_data.append(ti)
70
- except Exception:
73
+ except Exception as exc:
74
+ logger.debug("plan_arrangement failed: %s", exc)
71
75
  track_data.append({"index": t_idx, "name": track.get("name", ""), "devices": []})
72
76
 
73
77
  for section in sections:
@@ -96,7 +100,6 @@ def plan_arrangement(
96
100
  result["available_styles"] = sorted(planner_engine.VALID_STYLES)
97
101
  return result
98
102
 
99
-
100
103
  # ── transform_section (Round 4) ─────────────────────────────────────
101
104
 
102
105
 
@@ -130,6 +133,7 @@ def transform_section(
130
133
  track_count = session.get("track_count", 0)
131
134
 
132
135
  from .composition import _build_clip_matrix
136
+
133
137
  clip_matrix = _build_clip_matrix(ableton, len(scenes), track_count)
134
138
  sections = comp_engine.build_section_graph_from_scenes(scenes, clip_matrix, track_count)
135
139
 
@@ -17,6 +17,9 @@ from fastmcp import Context
17
17
  from ..server import mcp
18
18
  from ..memory.technique_store import TechniqueStore
19
19
  from . import _research_engine as research_engine
20
+ import logging
21
+
22
+ logger = logging.getLogger(__name__)
20
23
 
21
24
  _memory_store = TechniqueStore()
22
25
 
@@ -66,8 +69,8 @@ def research_technique(
66
69
  if ref and not ref.get("error") and ref.get("count", 0) > 0:
67
70
  device_atlas_results.append(ref)
68
71
  break # Found in this category, skip others
69
- except Exception:
70
- pass
72
+ except Exception as exc:
73
+ logger.debug("research_technique failed: %s", exc)
71
74
 
72
75
  # 3. Search memory for related techniques (direct TechniqueStore)
73
76
  memory_results = []
@@ -75,16 +78,16 @@ def research_technique(
75
78
  memory_results.extend(
76
79
  _memory_store.list_techniques(type_filter="technique_card", sort_by="updated_at", limit=10)
77
80
  )
78
- except Exception:
79
- pass
81
+ except Exception as exc:
82
+ logger.debug("research_technique failed: %s", exc)
80
83
 
81
84
  try:
82
85
  # "research" is not a valid type in TechniqueStore — search broadly
83
86
  memory_results.extend(
84
87
  _memory_store.search(query=query, limit=5)
85
88
  )
86
- except Exception:
87
- pass
89
+ except Exception as exc:
90
+ logger.debug("research_technique failed: %s", exc)
88
91
 
89
92
  if scope == "targeted":
90
93
  result = research_engine.targeted_research(
@@ -159,7 +162,8 @@ def get_emotional_arc(ctx: Context) -> dict:
159
162
  hf.mode = detected.get("mode", "")
160
163
  hf.confidence = detected.get("confidence", 0.0)
161
164
  break
162
- except Exception:
165
+ except Exception as exc:
166
+ logger.debug("get_emotional_arc failed: %s", exc)
163
167
  continue
164
168
  harmony_fields.append(hf)
165
169
 
@@ -186,7 +190,6 @@ def get_emotional_arc(ctx: Context) -> dict:
186
190
  "section_count": len(sections),
187
191
  }
188
192
 
189
-
190
193
  # ── get_style_tactics (Round 4) ─────────────────────────────────────
191
194
 
192
195
 
@@ -213,8 +216,8 @@ def get_style_tactics(
213
216
  memory_tactics = _memory_store.search(
214
217
  query=artist_or_genre, limit=10,
215
218
  )
216
- except Exception:
217
- pass
219
+ except Exception as exc:
220
+ logger.debug("get_style_tactics failed: %s", exc)
218
221
 
219
222
  tactics = research_engine.get_style_tactics(artist_or_genre, memory_tactics)
220
223
 
@@ -14,6 +14,10 @@ from ..tools import _composition_engine as comp_engine
14
14
  from .archetypes import TRANSITION_ARCHETYPES, select_archetype
15
15
  from .critics import run_all_transition_critics
16
16
  from .models import TransitionBoundary, TransitionPlan, TransitionScore
17
+ import logging
18
+
19
+ logger = logging.getLogger(__name__)
20
+
17
21
 
18
22
 
19
23
  # ── Helpers ───────────────────────────────────────────────────────────
@@ -29,7 +33,8 @@ def _build_sections_from_ableton(ctx: Context) -> list[comp_engine.SectionNode]:
29
33
  try:
30
34
  matrix_data = ableton.send_command("get_scene_matrix")
31
35
  clip_matrix = matrix_data.get("matrix", [])
32
- except Exception:
36
+ except Exception as exc:
37
+ logger.debug("_build_sections_from_ableton failed: %s", exc)
33
38
  clip_matrix = [[] for _ in range(len(scenes))]
34
39
 
35
40
  return comp_engine.build_section_graph_from_scenes(
@@ -33,9 +33,8 @@ def _fetch_translation_data(ctx: Context) -> dict:
33
33
  spec_data = spectral.get("spectrum")
34
34
  if spec_data and isinstance(spec_data["value"], dict):
35
35
  spectrum_bands = spec_data["value"]
36
- except Exception:
37
- pass
38
-
36
+ except Exception as exc:
37
+ logger.debug("_fetch_translation_data failed: %s", exc)
39
38
  # Estimate stereo width from track pans via session info
40
39
  stereo_width = 0.0
41
40
  center_strength = 0.5
@@ -61,9 +60,8 @@ def _fetch_translation_data(ctx: Context) -> dict:
61
60
 
62
61
  # Simple foreground detection: at least one unmuted, non-quiet track
63
62
  has_foreground = any(not t.get("muted", False) for t in tracks)
64
- except Exception:
65
- pass
66
-
63
+ except Exception as exc:
64
+ logger.debug("_get_pan failed: %s", exc)
67
65
  return {
68
66
  "stereo_width": stereo_width,
69
67
  "center_strength": center_strength,
@@ -99,6 +97,10 @@ def get_translation_issues(ctx: Context) -> dict:
99
97
 
100
98
  Lighter than check_translation — returns only detected issues
101
99
  from the 5 playback robustness critics.
100
+ import logging
101
+
102
+ logger = logging.getLogger(__name__)
103
+
102
104
  """
103
105
  mix_snapshot = _fetch_translation_data(ctx)
104
106
  issues = run_all_translation_critics(mix_snapshot)
@@ -9,9 +9,12 @@ from __future__ import annotations
9
9
 
10
10
  import hashlib
11
11
  import json
12
+ import logging
12
13
  import math
13
14
  from typing import Optional
14
15
 
16
+ logger = logging.getLogger(__name__)
17
+
15
18
 
16
19
  # ── Move discovery ───────────────────────────────────────────────
17
20
 
@@ -104,8 +107,9 @@ def discover_moves(
104
107
  if validation["valid"]:
105
108
  filtered.append(move)
106
109
  result = filtered
107
- except Exception:
108
- pass # constraint filtering is optional
110
+ except Exception as exc:
111
+ # constraint filtering is optional — keep the unfiltered list
112
+ logger.warning("constraint filtering skipped: %s", exc)
109
113
 
110
114
  return result
111
115
 
@@ -212,7 +216,8 @@ def _compile_variant_plan(move_dict: dict, kernel: dict | None) -> dict | None:
212
216
  try:
213
217
  plan = sem_compile(move_obj, kernel)
214
218
  return plan.to_dict()
215
- except Exception:
219
+ except Exception as exc:
220
+ logger.warning("sem_compile(%s) failed: %s", move_obj, exc)
216
221
  return None
217
222
 
218
223